on xenial help sign in
Application menus
commit90770d52195e593440a516558c68ff4f39429689 authorMichał Sawicz <michal.sawicz@canonical.com> dateTue 10 Jan 2017, 14:43 (10 years ago) parentcb0eedfb3162da
61 files changed+3528−863
debian/control+3−3 debian/unity8.install+1 plugins/GlobalShortcut/globalshortcut.cpp+12−4 plugins/GlobalShortcut/globalshortcut.h+5 plugins/GlobalShortcut/globalshortcutregistry.cpp+14−4 plugins/Unity/Indicators/sharedunitymenumodel.cpp+4 qml/ApplicationMenus/ApplicationMenuItemFactory.qml+135 qml/ApplicationMenus/MenuBar.qml+304 qml/ApplicationMenus/MenuItem.qml+150 qml/ApplicationMenus/MenuNavigator.qml+78 qml/ApplicationMenus/MenuPopup.qml+361 qml/ApplicationMenus/RegisteredApplicationMenuModel.qml+56 qml/CMakeLists.txt+1 qml/Components/DragHandle.qml+1 qml/Components/PanelState/PanelState.qml+6−3 qml/Components/ScrollCalculator.qml+21−2 qml/Components/WindowControlButtons.qml+21 qml/OrientedShell.qml+6 qml/Panel/FakePanelMenu.qml+26 qml/Panel/IndicatorPage.qml−188 qml/Panel/IndicatorItem.qml → qml/Panel/Indicators/IndicatorItem.qml+2−1 qml/Panel/Indicators/MenuItemFactory.qml → qml/Panel/Indicators/IndicatorMenuItemFactory.qml+7−10 qml/Panel/MenuContent.qml+7−10 qml/Panel/Panel.qml+300−96 qml/Panel/IndicatorsBar.qml → qml/Panel/PanelBar.qml+69−36 qml/Panel/IndicatorItemRow.qml → qml/Panel/PanelItemRow.qml+51−100 qml/Panel/IndicatorsMenu.qml → qml/Panel/PanelMenu.qml+31−19 qml/Panel/PanelMenuPage.qml+225 qml/Rotation/RotationStates.qml+2−2 qml/Shell.qml+20−10 qml/Stage/DecoratedWindow.qml+77−35 qml/Stage/Stage.qml+17−3 qml/Stage/WindowDecoration.qml+80−19 src/main.cpp+2−2 tests/mocks/QMenuModel/unitymenumodel.cpp+47−6 tests/mocks/QMenuModel/unitymenumodel.h+3−1 tests/mocks/Unity/Application/SurfaceManager.cpp+5−2 tests/mocks/Unity/Application/SurfaceManager.h+3−1 tests/mocks/Unity/ApplicationMenu/mockapplicationmenuregistry.cpp+5 tests/mocks/Unity/ApplicationMenu/mockapplicationmenuregistry.h+3 tests/mocks/Unity/Indicators/fakeindicatorsmodeldata.js+4−4 tests/qmltests/ApplicationMenuDataLoader.qml+75 tests/qmltests/ApplicationMenus/tst_MenuBar.qml+183 tests/qmltests/ApplicationMenus/tst_MenuPopup.qml+207 tests/qmltests/CMakeLists.txt+11−7 tests/qmltests/Panel/tst_IndicatorItem.qml → tests/qmltests/Panel/Indicators/tst_IndicatorItem.qml+1−1 tests/qmltests/Panel/Indicators/tst_MenuItemFactory.qml → tests/qmltests/Panel/Indicators/tst_IndicatorMenuItemFactory.qml+2−2 tests/qmltests/Panel/IndicatorTest.qml → tests/qmltests/Panel/PanelTest.qml−5 tests/qmltests/Panel/tst_MenuContent.qml+14−2 tests/qmltests/Panel/tst_Panel.qml+384−103 tests/qmltests/Panel/tst_IndicatorsBar.qml → tests/qmltests/Panel/tst_PanelBar.qml+24−33 tests/qmltests/Panel/tst_IndicatorItemRow.qml → tests/qmltests/Panel/tst_PanelItemRow.qml+36−32 tests/qmltests/Panel/tst_IndicatorsMenu.qml → tests/qmltests/Panel/tst_PanelMenu.qml+32−14 tests/qmltests/Panel/tst_IndicatorPage.qml → tests/qmltests/Panel/tst_PanelMenuPage.qml+53−47 tests/qmltests/Stage/tst_DecoratedWindow.qml+40−5 tests/qmltests/Stage/tst_DesktopStage.qml+9−45 tests/qmltests/Stage/tst_PhoneStage.qml+5 tests/qmltests/Stage/tst_TabletStage.qml+6 tests/qmltests/Stage/tst_WindowDecoration.qml+244 tests/qmltests/tst_Shell.qml+34−5 tests/qmltests/tst_ShellWithPin.qml+3−1
debian/control+3−3
@@ -29,7 +29,7 @@ Build-Depends: cmake,
2929 libpam0g-dev,
3030 libpay2-dev,
3131 libpulse-dev,
32− libqmenumodel-dev (>= 0.2.10),
32+ libqmenumodel-dev (>= 0.2.11),
3333 libqt5svg5-dev,
3434 libqt5xmlpatterns5-dev,
3535 libsystemsettings-dev,
@@ -83,7 +83,7 @@ X-Ubuntu-Use-Langpack: yes
8383
8484Package: indicators-client
8585Architecture: any
86−Depends: qmenumodel-qml (>= 0.2.10),
86+Depends: qmenumodel-qml (>= 0.2.11),
8787 qml-module-ubuntu-components (>= 1.3.2030) | qml-module-ubuntu-components-gles (>= 1.3.2030),
8888 unity8 (= ${binary:Version}),
8989 ${misc:Depends},
@@ -122,7 +122,7 @@ Depends: dmz-cursor-theme,
122122 gsettings-desktop-schemas,
123123 libcap2-bin,
124124 libglib2.0-bin,
125− qmenumodel-qml (>= 0.2.10),
125+ qmenumodel-qml (>= 0.2.11),
126126 qml-module-biometryd,
127127 qml-module-qt-labs-folderlistmodel,
128128 qml-module-qt-labs-settings,
debian/unity8.install+1
@@ -8,6 +8,7 @@ usr/bin/unity8
88usr/bin/unity8-dash
99usr/share/applications/unity8-dash.desktop
1010usr/share/applications/unity8.desktop
11+usr/share/unity8/ApplicationMenus
1112usr/share/unity8/Greeter
1213usr/share/unity8/Launcher
1314usr/share/unity8/Panel
plugins/GlobalShortcut/globalshortcut.cpp+12−4
@@ -63,10 +63,18 @@ void GlobalShortcut::componentComplete()
6363
6464void GlobalShortcut::keyPressEvent(QKeyEvent * event)
6565{
66− Q_UNUSED(event)
67− if (m_active) {
68− Q_EMIT triggered(m_shortcut.toString());
69− }
66+ if (!m_active) return;
67+
68+ event->accept();
69+ Q_EMIT triggered(m_shortcut.toString());
70+}
71+
72+void GlobalShortcut::keyReleaseEvent(QKeyEvent * event)
73+{
74+ if (!m_active) return;
75+
76+ event->accept();
77+ Q_EMIT released(m_shortcut.toString());
7078}
7179
7280void GlobalShortcut::setupFilterOnWindow(QQuickWindow *window)
plugins/GlobalShortcut/globalshortcut.h+5
@@ -55,6 +55,7 @@ public:
5555protected:
5656 void componentComplete() override;
5757 void keyPressEvent(QKeyEvent * event) override;
58+ void keyReleaseEvent(QKeyEvent * event) override;
5859
5960Q_SIGNALS:
6061 void shortcutChanged(const QVariant &shortcut);
@@ -62,6 +63,10 @@ Q_SIGNALS:
6263 * Emitted when a global keypress of @p shortcut is detected
6364 */
6465 void triggered(const QString &shortcut);
66+ /**
67+ * Emitted when a global keypress of @p shortcut is released
68+ */
69+ void released(const QString &shortcut);
6570 void activeChanged(bool active);
6671
6772private Q_SLOTS:
plugins/GlobalShortcut/globalshortcutregistry.cpp+14−4
@@ -73,20 +73,30 @@ bool GlobalShortcutRegistry::eventFilter(QObject *obj, QEvent *event)
7373 Q_ASSERT(m_filteredWindow);
7474 Q_ASSERT(obj == static_cast<QObject*>(m_filteredWindow.data()));
7575
76− if (event->type() == QEvent::KeyPress) {
76+ if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
77+
7778 QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
79+
80+ // Make a copy of the event so we don't alter it for passing on.
81+ QKeyEvent eCopy(keyEvent->type(),
82+ keyEvent->key(),
83+ keyEvent->modifiers(),
84+ keyEvent->text(),
85+ keyEvent->isAutoRepeat(),
86+ keyEvent->count());
87+ eCopy.ignore();
88+
7889 int seq = keyEvent->key() + keyEvent->modifiers();
7990 if (m_shortcuts.contains(seq)) {
8091 const auto shortcuts = m_shortcuts.value(seq);
8192 Q_FOREACH(const auto &shortcut, shortcuts) {
8293 if (shortcut) {
83− qApp->sendEvent(shortcut, keyEvent);
84− event->accept();
94+ qApp->sendEvent(shortcut, &eCopy);
8595 }
8696 }
8797 }
8898
89− return event->isAccepted();
99+ return eCopy.isAccepted();
90100 }
91101
92102 return QObject::eventFilter(obj, event);
plugins/Unity/Indicators/sharedunitymenumodel.cpp+4
@@ -81,12 +81,16 @@ void SharedUnityMenuModel::initialize()
8181 }
8282 } else {
8383 QSharedPointer<UnityMenuModel> model = UnityMenuModelCache::singleton()->model(m_menuObjectPath);
84+
8485 if (model != m_model) {
8586 if (model->busName() != m_busName) model->setBusName(m_busName);
8687 if (model->actions() != m_actions) model->setActions(m_actions);
8788
8889 m_model = model;
8990 Q_EMIT modelChanged();
91+ } else if (m_model) {
92+ if (m_model->busName() != m_busName) m_model->setBusName(m_busName);
93+ if (m_model->actions() != m_actions) m_model->setActions(m_actions);
9094 }
9195 }
9296}
qml/ApplicationMenus/ApplicationMenuItemFactory.qmlnew file+135
@@ -0,0 +1,135 @@
1+/*
2+ * Copyright 2016 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Ubuntu.Settings.Menus 0.1 as Menus
19+import Ubuntu.Settings.Components 0.1
20+import QMenuModel 0.1
21+import Utils 0.1 as Utils
22+import Ubuntu.Components 1.3
23+
24+Object {
25+ id: menuFactory
26+
27+ property string context
28+ property var rootModel: null
29+ property var menuModel: null
30+
31+ Component {
32+ id: applicationMenu;
33+
34+ ListItem {
35+ property QtObject menuData: null
36+ property int menuIndex: -1
37+
38+ height: layout.height
39+ enabled: menuData && menuData.sensitive || false
40+ divider.visible: false
41+
42+ onClicked: {
43+ menuModel.activate(menuIndex);
44+ }
45+
46+ Action {
47+ id: action
48+ text: menuData.label.replace("_", "&")
49+ }
50+
51+ ListItemLayout {
52+ id: layout
53+ title.text: action.text
54+
55+ Icon {
56+ source: menuData && menuData.icon || ""
57+ SlotsLayout.position: SlotsLayout.Leading
58+ height: units.gu(3)
59+ }
60+
61+ Label {
62+ text: menuData.shortcut
63+ visible: menuData.shortcut && QuickUtils.keyboardAttached
64+ SlotsLayout.position: SlotsLayout.Trailing
65+ color: enabled ? theme.palette.normal.backgroundSecondaryText :
66+ theme.palette.disabled.backgroundSecondaryText
67+ }
68+ }
69+ }
70+ }
71+
72+ Component {
73+ id: submenu
74+
75+ ListItem {
76+ property QtObject menuData: null
77+ property int menuIndex: -1
78+
79+ height: layout.height
80+ enabled: menuData && menuData.sensitive || false
81+ divider.visible: false
82+
83+ onClicked: {
84+ menuModel.activate(menuIndex);
85+ }
86+
87+ Action {
88+ id: action
89+ text: menuData.label.replace("_", "&")
90+ }
91+
92+ ListItemLayout {
93+ id: layout
94+ title.text: action.text
95+
96+ Icon {
97+ source: menuData && menuData.icon || ""
98+ SlotsLayout.position: SlotsLayout.Leading
99+ height: units.gu(3)
100+ }
101+
102+ Icon {
103+ name: "toolkit_chevron-ltr_1gu"
104+ SlotsLayout.position: SlotsLayout.Trailing
105+ width: units.gu(2)
106+ color: enabled ? theme.palette.normal.backgroundSecondaryText :
107+ theme.palette.disabled.backgroundSecondaryText
108+ }
109+ }
110+ }
111+ }
112+
113+ Component {
114+ id: applicationMenuSeparator;
115+
116+ Menus.SeparatorMenu {
117+ objectName: "separatorMenu"
118+ }
119+ }
120+
121+ function load(modelData) {
122+ if (modelData.isSeparator) {
123+ return applicationMenuSeparator;
124+ }
125+ if (modelData.isRadio) {
126+ }
127+ if (modelData.isCheck) {
128+
129+ }
130+ if (modelData.hasSubmenu) {
131+ return submenu;
132+ }
133+ return applicationMenu;
134+ }
135+}
qml/ApplicationMenus/MenuBar.qmlnew file+304
@@ -0,0 +1,304 @@
1+/*
2+ * Copyright 2016 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
19+import Utils 0.1
20+import Ubuntu.Components 1.3
21+import GlobalShortcut 1.0
22+
23+Item {
24+ id: root
25+ objectName: "menuBar"
26+
27+ property alias unityMenuModel: rowRepeater.model
28+
29+ readonly property bool valid: rowRepeater.count > 0
30+
31+ property bool enableKeyFilter: false
32+
33+ readonly property bool showRequested: d.longAltPressed || d.currentItem != null
34+
35+ implicitWidth: row.width
36+ height: parent.height
37+
38+ function select(index) {
39+ d.select(index);
40+ }
41+
42+ function dismiss() {
43+ d.dismissAll();
44+ }
45+
46+ GlobalShortcut {
47+ shortcut: Qt.Key_Alt|Qt.AltModifier
48+ active: enableKeyFilter
49+ onTriggered: d.startShortcutTimer()
50+ onReleased: d.stopSHortcutTimer()
51+ }
52+ // On an actual keyboard, the AltModifier is not supplied on release.
53+ GlobalShortcut {
54+ shortcut: Qt.Key_Alt
55+ active: enableKeyFilter
56+ onTriggered: d.startShortcutTimer()
57+ onReleased: d.stopSHortcutTimer()
58+ }
59+
60+ InverseMouseArea {
61+ acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
62+ anchors.fill: parent
63+ enabled: d.currentItem != null
64+ onPressed: d.dismissAll()
65+ }
66+
67+ Item {
68+ id: clippingItem
69+
70+ height: root.height
71+ width: root.width
72+ clip: true
73+
74+ Row {
75+ id: row
76+ spacing: units.gu(2)
77+ height: parent.height
78+
79+ ActionContext {
80+ id: menuBarContext
81+ objectName: "barContext"
82+ active: !d.currentItem && enableKeyFilter
83+ }
84+
85+ Repeater {
86+ id: rowRepeater
87+
88+ Item {
89+ id: visualItem
90+ objectName: root.objectName + "-item" + __ownIndex
91+
92+ readonly property int __ownIndex: index
93+ property Item __popup: null;
94+ property bool popupVisible: __popup && __popup.visible
95+
96+ implicitWidth: column.implicitWidth
97+ implicitHeight: row.height
98+ enabled: model.sensitive
99+
100+ function show() {
101+ if (!__popup) {
102+ __popup = menuComponent.createObject(root, { objectName: visualItem.objectName + "-menu" });
103+ // force the current item to be the newly popped up menu
104+ } else {
105+ __popup.show();
106+ }
107+ d.currentItem = visualItem;
108+ }
109+ function hide() {
110+ if (__popup) {
111+ __popup.hide();
112+
113+ if (d.currentItem === visualItem) {
114+ d.currentItem = null;
115+ }
116+ }
117+ }
118+ function dismiss() {
119+ if (__popup) {
120+ __popup.destroy();
121+ __popup = null;
122+
123+ if (d.currentItem === visualItem) {
124+ d.currentItem = null;
125+ }
126+ }
127+ }
128+
129+ Connections {
130+ target: d
131+ onDismissAll: visualItem.dismiss()
132+ }
133+
134+ Component {
135+ id: menuComponent
136+ MenuPopup {
137+ x: visualItem.x - units.gu(1)
138+ anchors.top: parent.bottom
139+ unityMenuModel: root.unityMenuModel.submenu(visualItem.__ownIndex)
140+
141+ Component.onCompleted: reset();
142+ }
143+ }
144+
145+ RowLayout {
146+ id: column
147+ spacing: units.gu(1)
148+ anchors {
149+ centerIn: parent
150+ }
151+
152+ Icon {
153+ Layout.preferredWidth: units.gu(2)
154+ Layout.preferredHeight: units.gu(2)
155+ Layout.alignment: Qt.AlignVCenter
156+
157+ visible: model.icon || false
158+ source: model.icon || ""
159+ }
160+
161+ ActionItem {
162+ id: actionItem
163+ width: _title.width
164+ height: _title.height
165+
166+ action: Action {
167+ // FIXME - SDK Action:text modifies menu text with html underline for mnemonic
168+ text: model.label.replace("_", "&").replace("<u>", "&").replace("</u>", "")
169+
170+ onTriggered: {
171+ visualItem.show();
172+ }
173+ }
174+
175+ Label {
176+ id: _title
177+ text: actionItem.text
178+ horizontalAlignment: Text.AlignLeft
179+ color: enabled ? "white" : "#5d5d5d"
180+ }
181+ }
182+ }
183+ } // Item ( delegate )
184+ } // Repeater
185+ } // Row
186+
187+ MouseArea {
188+ anchors.fill: parent
189+ hoverEnabled: d.currentItem
190+
191+ onEntered: {
192+ if (d.currentItem) {
193+ updateCurrentItemFromPosition(Qt.point(mouseX, mouseY))
194+ }
195+ }
196+ onPositionChanged: {
197+ if (d.currentItem) {
198+ updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
199+ }
200+ }
201+ onClicked: updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
202+
203+ function updateCurrentItemFromPosition(point) {
204+ var pos = mapToItem(row, point.x, point.y);
205+
206+ if (!d.hoveredItem || !d.currentItem || !d.hoveredItem.contains(Qt.point(pos.x - d.currentItem.x, pos.y - d.currentItem.y))) {
207+ d.hoveredItem = row.childAt(pos.x, pos.y);
208+ if (!d.hoveredItem || !d.hoveredItem.enabled)
209+ return false;
210+ if (d.currentItem != d.hoveredItem) {
211+ d.currentItem = d.hoveredItem;
212+ }
213+ }
214+ return true;
215+ }
216+ }
217+
218+ Rectangle {
219+ id: underline
220+ anchors {
221+ bottom: row.bottom
222+ }
223+ x: d.currentItem ? row.x + d.currentItem.x - units.gu(1) : 0
224+ width: d.currentItem ? d.currentItem.width + units.gu(2) : 0
225+ height: units.dp(4)
226+ color: UbuntuColors.orange
227+ visible: d.currentItem
228+ }
229+ }
230+
231+ MenuNavigator {
232+ id: d
233+ objectName: "d"
234+ itemView: rowRepeater
235+
236+ property Item currentItem: null
237+ property Item hoveredItem: null
238+ property Item prevCurrentItem: null
239+
240+ readonly property int currentIndex: currentItem ? currentItem.__ownIndex : -1
241+
242+ property bool altPressed: false
243+ property bool longAltPressed: false
244+
245+ signal dismissAll()
246+
247+ onSelect: {
248+ var delegate = rowRepeater.itemAt(index);
249+ if (delegate) {
250+ d.currentItem = delegate;
251+ }
252+ }
253+
254+ onCurrentItemChanged: {
255+ if (prevCurrentItem && prevCurrentItem != currentItem) {
256+ if (currentItem) {
257+ prevCurrentItem.hide();
258+ } else {
259+ prevCurrentItem.dismiss();
260+ }
261+ }
262+
263+ if (currentItem) currentItem.show();
264+ prevCurrentItem = currentItem;
265+ }
266+
267+ function startShortcutTimer() {
268+ d.altPressed = true;
269+ menuBarShortcutTimer.start();
270+ }
271+
272+ function stopSHortcutTimer() {
273+ menuBarShortcutTimer.stop();
274+ d.altPressed = false;
275+ d.longAltPressed = false;
276+ }
277+ }
278+
279+ Timer {
280+ id: menuBarShortcutTimer
281+ interval: 200
282+ repeat: false
283+ onTriggered: {
284+ d.longAltPressed = true;
285+ }
286+ }
287+
288+ Keys.onEscapePressed: {
289+ d.dismissAll();
290+ event.accepted = true;
291+ }
292+
293+ Keys.onLeftPressed: {
294+ if (d.currentItem) {
295+ d.selectPrevious(d.currentIndex);
296+ }
297+ }
298+
299+ Keys.onRightPressed: {
300+ if (d.currentItem) {
301+ d.selectNext(d.currentIndex);
302+ }
303+ }
304+}
qml/ApplicationMenus/MenuItem.qmlnew file+150
@@ -0,0 +1,150 @@
1+/*
2+ * Copyright 2016 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
19+import Ubuntu.Components 1.3
20+
21+ActionItem {
22+ id: root
23+ implicitHeight: units.gu(5)
24+ implicitWidth: requiredWidth
25+
26+ property var menuData: undefined
27+
28+ readonly property real requiredWidth: {
29+ var val = 0;
30+ val += units.gu(1) + flagGutter.width;
31+ if (iconSource != "") {
32+ val += units.gu(1) + icon.width
33+ }
34+ val += units.gu(1) + title.contentWidth;
35+ if (hasSubmenu) {
36+ val += units.gu(1) + chevronIcon.width;
37+ } else if (shortcut != undefined) {
38+ val += units.gu(3) + shortcutLabel.contentWidth;
39+ }
40+ return val + units.gu(1);
41+ }
42+
43+ readonly property bool hasSubmenu: menuData ? menuData.hasSubmenu : false
44+ readonly property bool _checked : action && action.checkable ? action.checked : false
45+
46+ enabled: menuData ? menuData.sensitive : false
47+
48+ action: Action {
49+ // FIXME - SDK Action:text modifies menu text with html underline for mnemonic
50+ text: menuData.label.replace("_", "&").replace("<u>", "&").replace("</u>", "")
51+ checkable: menuData.isCheck || menuData.isRadio
52+ checked: menuData.isToggled
53+ }
54+
55+ width: {
56+ if (!parent) return implicitWidth;
57+ if (parent.width > implicitWidth) return parent.width;
58+ return implicitWidth;
59+ }
60+
61+ Keys.onRightPressed: {
62+ if (hasSubmenu) {
63+ root.trigger();
64+ } else {
65+ event.accepted = false;
66+ }
67+ }
68+ Keys.onReturnPressed: {
69+ root.trigger();
70+ }
71+ Keys.onEnterPressed: {
72+ root.trigger();
73+ }
74+
75+ RowLayout {
76+ id: row
77+ spacing: units.gu(1)
78+ anchors.left: parent.left
79+ anchors.right: parent.right
80+ anchors.leftMargin: units.gu(1)
81+ anchors.rightMargin: units.gu(1)
82+ anchors.verticalCenter: parent.verticalCenter
83+
84+ Item {
85+ Layout.minimumWidth: units.gu(1.5)
86+ Layout.minimumHeight: units.gu(1.5)
87+
88+ Icon {
89+ id: flagGutter
90+ width: units.gu(1.5)
91+ height: units.gu(1.5)
92+ visible: _checked
93+ name: "tick"
94+ }
95+ }
96+
97+ Icon {
98+ id: icon
99+ width: units.gu(2)
100+ height: units.gu(2)
101+
102+ visible: root.iconSource != "" || false
103+ source: root.iconSource || ""
104+ }
105+
106+ RowLayout {
107+ spacing: units.gu(3)
108+
109+ Label {
110+ id: title
111+ elide: Text.ElideNone
112+ wrapMode: Text.NoWrap
113+ clip: true
114+ Layout.fillWidth: true
115+
116+ text: root.text ? root.text : ""
117+ }
118+
119+ Label {
120+ id: shortcutLabel
121+ elide: Text.ElideNone
122+ wrapMode: Text.NoWrap
123+ clip: true
124+ color: enabled ? theme.palette.normal.backgroundSecondaryText :
125+ theme.palette.disabled.backgroundSecondaryText
126+
127+ visible: menuData.shortcut != undefined && !root.hasSubmenu && QuickUtils.keyboardAttached
128+ text: menuData.shortcut ? menuData.shortcut : ""
129+ }
130+ }
131+
132+ Icon {
133+ id: chevronIcon
134+ width: units.gu(2)
135+ height: units.gu(2)
136+ color: enabled ? theme.palette.normal.backgroundSecondaryText :
137+ theme.palette.disabled.backgroundSecondaryText
138+
139+ visible: root.hasSubmenu
140+ name: "chevron"
141+ }
142+ }
143+
144+ MouseArea {
145+ anchors.fill: parent
146+ onClicked: {
147+ root.trigger(action && action.checkable ? !action.checked : undefined);
148+ }
149+ }
150+}
qml/ApplicationMenus/MenuNavigator.qmlnew file+78
@@ -0,0 +1,78 @@
1+/*
2+ * Copyright 2016 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+
19+QtObject {
20+ property Item itemView: null
21+
22+ signal select(int index)
23+
24+ function selectNext(currentIndex) {
25+ var menu;
26+ var newIndex = 0;
27+ if (currentIndex === -1 && itemView.count > 0) {
28+ while (itemView.count > newIndex) {
29+ menu = itemView.itemAt(newIndex);
30+ if (!!menu["enabled"]) {
31+ select(newIndex);
32+ break;
33+ }
34+ newIndex++;
35+ }
36+ } else if (currentIndex !== -1 && itemView.count > 1) {
37+ var startIndex = (currentIndex + 1) % itemView.count;
38+ newIndex = startIndex;
39+ do {
40+ menu = itemView.itemAt(newIndex);
41+ if (!!menu["enabled"]) {
42+ select(newIndex);
43+ break;
44+ }
45+ newIndex = (newIndex + 1) % itemView.count;
46+ } while (newIndex !== startIndex)
47+ }
48+ }
49+
50+ function selectPrevious(currentIndex) {
51+ var menu;
52+ var newIndex = itemView.count-1;
53+ if (currentIndex === -1 && itemView.count > 0) {
54+ while (itemView.count > newIndex) {
55+ menu = itemView.itemAt(newIndex);
56+ if (!!menu["enabled"]) {
57+ select(newIndex);
58+ break;
59+ }
60+ newIndex--;
61+ }
62+ } else if (currentIndex !== -1 && itemView.count > 1) {
63+ var startIndex = currentIndex - 1;
64+ newIndex = startIndex;
65+ do {
66+ if (newIndex < 0) {
67+ newIndex = itemView.count - 1;
68+ }
69+ menu = itemView.itemAt(newIndex);
70+ if (!!menu["enabled"]) {
71+ select(newIndex);
72+ break;
73+ }
74+ newIndex--;
75+ } while (newIndex !== startIndex)
76+ }
77+ }
78+}
qml/ApplicationMenus/MenuPopup.qmlnew file+361
@@ -0,0 +1,361 @@
1+/*
2+ * Copyright 2016 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
19+import QtQuick.Window 2.2
20+import Ubuntu.Components 1.3
21+import Ubuntu.Components.ListItems 1.3 as ListItems
22+import "../Components"
23+
24+UbuntuShape {
25+ id: root
26+ objectName: "menu"
27+ backgroundColor: theme.palette.normal.overlay
28+
29+ property alias unityMenuModel: listView.model
30+
31+ readonly property real __ajustedMinimumHeight: {
32+ if (listView.contentHeight > __minimumHeight) {
33+ return units.gu(30);
34+ }
35+ return Math.max(listView.contentHeight, units.gu(2));
36+ }
37+
38+ readonly property real __minimumWidth: units.gu(20)
39+ readonly property real __minimumHeight: units.gu(30)
40+ readonly property real __maximumWidth: Screen.width * 0.7
41+ readonly property real __maximumHeight: Screen.height * 0.7
42+
43+ function show() {
44+ visible = true;
45+ focusScope.forceActiveFocus();
46+ }
47+
48+ function hide() {
49+ visible = false;
50+ d.currentItem = null;
51+ }
52+
53+ function select(index) {
54+ d.select(index)
55+ }
56+
57+ function reset() {
58+ d.currentItem = null;
59+ dismiss();
60+ }
61+
62+ function dismiss() {
63+ d.dismissAll();
64+ }
65+
66+ implicitWidth: container.width
67+ implicitHeight: MathUtils.clamp(listView.contentHeight, __ajustedMinimumHeight, __maximumHeight)
68+
69+ MenuNavigator {
70+ id: d
71+ objectName: "d"
72+ itemView: listView
73+
74+ property Item currentItem: null
75+ property Item hoveredItem: null
76+ readonly property int currentIndex: currentItem ? currentItem.__ownIndex : -1
77+
78+ signal dismissAll()
79+
80+ onCurrentItemChanged: {
81+ if (currentItem) {
82+ currentItem.item.forceActiveFocus();
83+ } else {
84+ hoveredItem = null;
85+ }
86+ }
87+
88+ onSelect: {
89+ currentItem = listView.itemAt(index);
90+ }
91+ }
92+
93+ Item {
94+ id: focusScope
95+ anchors.fill: parent
96+ focus: visible
97+
98+ Keys.onUpPressed: d.selectPrevious(d.currentIndex)
99+ Keys.onDownPressed: d.selectNext(d.currentIndex)
100+ Keys.onRightPressed: {
101+ // Don't let right keypresses fall through if the current item has a visible popup.
102+ if (!d.currentItem || !d.currentItem.popup || !d.currentItem.popup.visible) {
103+ event.accepted = false;
104+ }
105+ }
106+
107+ ColumnLayout {
108+ id: container
109+ objectName: "container"
110+
111+ width: listView.contentWidth
112+ height: parent.height
113+ spacing: 0
114+
115+ // FIXME use ListView.header - tried but was flaky with positionViewAtIndex.
116+ Item {
117+ Layout.fillWidth: true;
118+ Layout.maximumHeight: units.gu(3)
119+ Layout.minimumHeight: units.gu(3)
120+ visible: listView.contentHeight > root.height
121+ enabled: !listView.atYBeginning
122+
123+ Rectangle {
124+ color: enabled ? theme.palette.normal.overlayText :
125+ theme.palette.disabled.overlayText
126+ height: units.dp(1)
127+ anchors {
128+ bottom: parent.bottom
129+ left: parent.left
130+ right: parent.right
131+ }
132+ }
133+
134+ Icon {
135+ anchors.centerIn: parent
136+ width: units.gu(2)
137+ height: units.gu(2)
138+ name: "up"
139+ color: enabled ? theme.palette.normal.overlayText :
140+ theme.palette.disabled.overlayText
141+ }
142+
143+ MouseArea {
144+ anchors.fill: parent
145+ onPressed: {
146+ var index = listView.indexAt(0, listView.contentY);
147+ listView.positionViewAtIndex(index-1, ListView.Beginning);
148+ }
149+ }
150+ }
151+
152+ ListView {
153+ id: listView
154+ objectName: "listView"
155+ Layout.fillHeight: true
156+ Layout.fillWidth: true
157+ contentWidth: MathUtils.clamp(contentItem.childrenRect.width,
158+ __minimumWidth,
159+ __maximumWidth)
160+
161+ orientation: Qt.Vertical
162+ interactive: contentHeight > height
163+ clip: interactive
164+ highlightFollowsCurrentItem: false
165+
166+ highlight: Rectangle {
167+ color: "transparent"
168+ border.width: units.dp(1)
169+ border.color: UbuntuColors.orange
170+ z: 1
171+
172+ width: listView.width
173+ height: d.currentItem ? d.currentItem.height : 0
174+ y: d.currentItem ? d.currentItem.y : 0
175+ visible: d.currentItem
176+ }
177+
178+ function itemAt(index) {
179+ if (index > count || index < 0) return null;
180+ currentIndex = index;
181+ return currentItem;
182+ }
183+
184+ MouseArea {
185+ id: menuMouseArea
186+ anchors.fill: listView
187+ hoverEnabled: true
188+ propagateComposedEvents: true // propogate events so we send clicks to children.
189+ z: 1 // on top so we override any other hovers
190+ onEntered: updateCurrentItemFromPosition(Qt.point(mouseX, mouseY))
191+ onPositionChanged: updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
192+
193+ function updateCurrentItemFromPosition(point) {
194+ var pos = mapToItem(listView.contentItem, point.x, point.y);
195+
196+ if (!d.hoveredItem || !d.currentItem ||
197+ !d.hoveredItem.contains(Qt.point(pos.x - d.currentItem.x, pos.y - d.currentItem.y))) {
198+ d.hoveredItem = listView.itemAt(listView.indexAt(pos.x, pos.y));
199+ if (!d.hoveredItem || !d.hoveredItem.enabled)
200+ return false;
201+ d.currentItem = d.hoveredItem;
202+ }
203+ return true;
204+ }
205+ }
206+
207+ ActionContext {
208+ id: menuBarContext
209+ objectName: "menuContext"
210+ active: {
211+ if (!root.visible) return false;
212+ if (d.currentItem && d.currentItem.popup && d.currentItem.popup.visible) {
213+ return false;
214+ }
215+ return true;
216+ }
217+ }
218+
219+ delegate: Loader {
220+ id: loader
221+ objectName: root.objectName + "-item" + __ownIndex
222+
223+ property int __ownIndex: index
224+
225+ width: root.width
226+ enabled: model.isSeparator ? false : model.sensitive
227+
228+ sourceComponent: {
229+ if (model.isSeparator) {
230+ return separatorComponent;
231+ }
232+ return menuItemComponent;
233+ }
234+
235+ property Item popup: null
236+
237+ Component {
238+ id: menuItemComponent
239+ MenuItem {
240+ id: menuItem
241+ menuData: model
242+ objectName: loader.objectName + "-actionItem"
243+
244+ action.onTriggered: {
245+ d.currentItem = loader;
246+
247+ if (hasSubmenu) {
248+ if (!popup) {
249+ var model = root.unityMenuModel.submenu(__ownIndex);
250+ popup = submenuComponent.createObject(focusScope, {
251+ objectName: loader.objectName + "-",
252+ unityMenuModel: model,
253+ x: Qt.binding(function() { return root.width }),
254+ y: Qt.binding(function() { return loader.y })
255+ });
256+ } else if (popup) {
257+ popup.visible = true;
258+ }
259+ popup.retreat.connect(function() {
260+ popup.destroy();
261+ popup = null;
262+ menuItem.forceActiveFocus();
263+ })
264+ } else {
265+ root.unityMenuModel.activate(__ownIndex);
266+ }
267+ }
268+
269+ Connections {
270+ target: d
271+ onCurrentIndexChanged: {
272+ if (popup && d.currentIndex != __ownIndex) {
273+ popup.visible = false;
274+ }
275+ }
276+ onDismissAll: {
277+ if (popup) {
278+ popup.destroy();
279+ popup = null;
280+ }
281+ }
282+ }
283+ }
284+ }
285+
286+ Component {
287+ id: separatorComponent
288+ ListItems.ThinDivider {
289+ objectName: loader.objectName + "-separator"
290+ }
291+ }
292+ }
293+ } // ListView
294+
295+ // FIXME use ListView.footer - tried but was flaky with positionViewAtIndex.
296+ Item {
297+ Layout.fillWidth: true;
298+ Layout.maximumHeight: units.gu(3)
299+ Layout.minimumHeight: units.gu(3)
300+ visible: listView.contentHeight > root.height
301+ enabled: !listView.atYEnd
302+
303+ Rectangle {
304+ color: enabled ? theme.palette.normal.overlayText :
305+ theme.palette.disabled.overlayText
306+ height: units.dp(1)
307+ anchors {
308+ top: parent.top
309+ left: parent.left
310+ right: parent.right
311+ }
312+ }
313+
314+ Icon {
315+ anchors.centerIn: parent
316+ width: units.gu(2)
317+ height: units.gu(2)
318+ name: "down"
319+ color: enabled ? theme.palette.normal.overlayText :
320+ theme.palette.disabled.overlayText
321+ }
322+
323+ MouseArea {
324+ anchors.fill: parent
325+ onPressed: {
326+ var index = listView.indexAt(0, listView.contentY);
327+ listView.positionViewAtIndex(index+1, ListView.Beginning);
328+ }
329+ }
330+ }
331+ } // Column
332+
333+ Component {
334+ id: submenuComponent
335+ Loader {
336+ id: submenuLoader
337+ source: "MenuPopup.qml"
338+
339+ property var unityMenuModel: null
340+ signal retreat()
341+
342+ Binding {
343+ target: item
344+ property: "unityMenuModel"
345+ value: submenuLoader.unityMenuModel
346+ }
347+
348+ Binding {
349+ target: item
350+ property: "objectName"
351+ value: submenuLoader.objectName + "menu"
352+ }
353+
354+ Keys.onLeftPressed: retreat()
355+
356+ Component.onCompleted: item.select(0);
357+ onVisibleChanged: if (visible) { item.select(0); }
358+ }
359+ }
360+ }
361+}
qml/ApplicationMenus/RegisteredApplicationMenuModel.qmlnew file+56
@@ -0,0 +1,56 @@
1+/*
2+ * Copyright 2016 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+import Unity.Indicators 0.1
20+import Unity.ApplicationMenu 0.1
21+import Unity.Indicators 0.1 as Indicators
22+
23+Object {
24+ property string persistentSurfaceId
25+ readonly property alias model: sharedAppModel.model
26+
27+ Indicators.SharedUnityMenuModel {
28+ id: sharedAppModel
29+ property var menus: ApplicationMenuRegistry.getMenusForSurface(persistentSurfaceId)
30+ property var menuService: menus.length > 0 ? menus[0] : null
31+
32+ busName: menuService ? menuService.service : ""
33+ menuObjectPath: menuService && menuService.menuPath ? menuService.menuPath : ""
34+ actions: menuService && menuService.actionPath ? { "unity": menuService.actionPath } : {}
35+ }
36+
37+ onPersistentSurfaceIdChanged: update()
38+
39+ function update() {
40+ sharedAppModel.menus = Qt.binding(function() { return ApplicationMenuRegistry.getMenusForSurface(persistentSurfaceId); });
41+ }
42+
43+ Connections {
44+ target: ApplicationMenuRegistry
45+ onSurfaceMenuRegistered: {
46+ if (surfaceId === persistentSurfaceId) {
47+ update();
48+ }
49+ }
50+ onSurfaceMenuUnregistered: {
51+ if (surfaceId === persistentSurfaceId) {
52+ update();
53+ }
54+ }
55+ }
56+}
qml/CMakeLists.txt+1
@@ -5,6 +5,7 @@ install(FILES ${QML_JS_FILES}
55 )
66
77set(QML_DIRS
8+ ApplicationMenus
89 Components
910 Dash
1011 graphics
qml/Components/DragHandle.qml+1
@@ -67,6 +67,7 @@ SwipeArea {
6767 SmoothedAnimation {
6868 id: hintingAnimation
6969 target: hintingAnimation
70+ objectName: "hintingAnimation"
7071 property: "targetValue"
7172 duration: 150
7273 velocity: -1
qml/Components/PanelState/PanelState.qml+6−3
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 2014-2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -21,8 +21,8 @@ QtObject {
2121 id: root
2222
2323 property string title: ""
24− property bool buttonsVisible: false
25− property bool buttonsAlwaysVisible: false
24+ property bool decorationsVisible: false
25+ property bool decorationsAlwaysVisible: false
2626 property bool closeButtonShown: true
2727 property bool dropShadow: false
2828 property int panelHeight: 0
@@ -30,4 +30,7 @@ QtObject {
3030 signal closeClicked()
3131 signal minimizeClicked()
3232 signal restoreClicked()
33+
34+ property string focusedPersistentSurfaceId: ""
35+ property bool focusedSurfaceMaximized: false
3336}
qml/Components/ScrollCalculator.qml+21−2
@@ -22,6 +22,7 @@ Item {
2222
2323 readonly property bool areaActive: lateralPosition >= 0
2424 property real stopScrollThreshold: units.gu(2)
25+ property real progressThreshold: units.dp(4)
2526 property int direction: Qt.LeftToRight
2627 property real baseScrollAmount: units.dp(3)
2728 property real maximumScrollAmount: units.dp(8)
@@ -38,6 +39,11 @@ Item {
3839 function handleEnter() {
3940 d.thresholdAreaX = -scrollArea.stopScrollThreshold;
4041 scrollTimer.restart();
42+ d.init = true;
43+ d.passedProgressThreshold = false;
44+
45+ d.progression = 0;
46+ d.startingProgression = 0;
4147 }
4248
4349 function handleExit() {
@@ -59,6 +65,10 @@ Item {
5965 }
6066
6167 d.progression = lateralPosition / width;
68+ if (d.init) {
69+ d.startingProgression = d.progression;
70+ d.init = false;
71+ }
6272 }
6373 }
6474
@@ -68,14 +78,23 @@ Item {
6878 repeat: true
6979
7080 onTriggered: {
71− var scrollAmount = scrollArea.baseScrollAmount + scrollArea.maximumScrollAmount * d.progression;
72− scrollArea.scroll(scrollAmount);
81+ if (d.passedProgressThreshold ||
82+ Math.abs(d.progression - d.startingProgression) * scrollArea.width > scrollArea.progressThreshold) {
83+ d.passedProgressThreshold = true;
84+
85+ var scrollAmount = scrollArea.baseScrollAmount + scrollArea.maximumScrollAmount * d.progression;
86+ scrollArea.scroll(scrollAmount);
87+ }
7388 }
7489 }
7590
7691 QtObject {
7792 id: d
93+ property real startingProgression: 0
94+ property bool init: true
95+
7896 property real progression: 0
7997 property real thresholdAreaX: -scrollArea.stopScrollThreshold
98+ property bool passedProgressThreshold: false
8099 }
81100}
qml/Components/WindowControlButtons.qml+21
@@ -46,6 +46,13 @@ Row {
4646 onClicked: root.closeClicked()
4747 visible: root.closeButtonShown
4848
49+ // We dont want touch events to fall through to parent,
50+ // otherwise the containsMouse will not work.
51+ MouseArea {
52+ anchors.fill: parent
53+ propagateComposedEvents: true
54+ }
55+
4956 Rectangle {
5057 anchors.fill: parent
5158 anchors.margins: windowIsMaximized ? units.dp(3) : 0
@@ -69,6 +76,13 @@ Row {
6976 width: height
7077 onClicked: root.minimizeClicked()
7178
79+ // We dont want touch events to fall through to parent,
80+ // otherwise the containsMouse will not work.
81+ MouseArea {
82+ anchors.fill: parent
83+ propagateComposedEvents: true
84+ }
85+
7286 Rectangle {
7387 anchors.fill: parent
7488 anchors.margins: windowIsMaximized ? units.dp(3) : 0
@@ -102,6 +116,13 @@ Row {
102116 }
103117 }
104118
119+ // We dont want touch events to fall through to parent,
120+ // otherwise the containsMouse will not work.
121+ MouseArea {
122+ anchors.fill: parent
123+ propagateComposedEvents: true
124+ }
125+
105126 Rectangle {
106127 anchors.fill: parent
107128 anchors.margins: windowIsMaximized ? units.dp(3) : 0
qml/OrientedShell.qml+6
@@ -98,6 +98,12 @@ Item {
9898 deviceFilter: InputInfo.TouchScreen
9999 }
100100
101+ Binding {
102+ target: QuickUtils
103+ property: "keyboardAttached"
104+ value: keyboardsModel.count > 0
105+ }
106+
101107 readonly property int pointerInputDevices: miceModel.count + touchPadModel.count
102108 onPointerInputDevicesChanged: calculateUsageMode()
103109
qml/Panel/FakePanelMenu.qmlnew file+26
@@ -0,0 +1,26 @@
1+/*
2+ * Copyright (C) 2013-2015 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+
19+QtObject {
20+ property list<QtObject> hides
21+ property QtObject model: null
22+ property bool expanded: false
23+
24+ signal hide()
25+ signal show()
26+}
qml/Panel/IndicatorPage.qmldeleted−188
@@ -1,188 +0,0 @@
1−/*
2− * Copyright 2013-2014 Canonical Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU Lesser General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU Lesser General Public License for more details.
12− *
13− * You should have received a copy of the GNU Lesser General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−import QtQuick 2.4
18−import Ubuntu.Components 1.3 as Components
19−import Unity.Indicators 0.1 as Indicators
20−import "../Components"
21−import "Indicators"
22−
23−IndicatorBase {
24− id: main
25−
26− //const
27− property string title: rootActionState.title || rootActionState.accessibleName // some indicators don't expose a title but only the accessible-desc
28− property alias highlightFollowsCurrentItem : mainMenu.highlightFollowsCurrentItem
29− readonly property alias factory: _factory
30−
31− Indicators.UnityMenuModelStack {
32− id: menuStack
33− head: main.menuModel
34−
35− property var rootMenu: null
36−
37− onTailChanged: {
38− if (!tail) {
39− rootMenu = null;
40− } else if (rootMenu != tail) {
41− if (tail.get(0, "type") === rootMenuType) {
42− rootMenu = menuStack.tail.submenu(0);
43− push(rootMenu, 0);
44− } else {
45− rootMenu = null;
46− }
47− }
48− }
49− }
50−
51− Connections {
52− target: menuStack.tail
53−
54− // fix async creation with signal from model before it's finished.
55− Component.onCompleted: update();
56− onRowsInserted: update();
57− onModelReset: update();
58−
59− function update() {
60− if (menuStack.rootMenu !== menuStack.tail && menuStack.tail.get(0, "type") === rootMenuType) {
61− menuStack.rootMenu = menuStack.tail.submenu(0);
62− menuStack.push(menuStack.rootMenu, 0);
63− }
64− }
65− }
66−
67− ListView {
68− id: mainMenu
69− objectName: "mainMenu"
70− model: menuStack.rootMenu
71−
72− anchors {
73− fill: parent
74− bottomMargin: Qt.inputMethod.visible ? (Qt.inputMethod.keyboardRectangle.height - main.anchors.bottomMargin) : 0
75−
76− Behavior on bottomMargin {
77− NumberAnimation {
78− duration: 175
79− easing.type: Easing.OutQuad
80− }
81− }
82− // TODO - does ever frame.
83− onBottomMarginChanged: {
84− mainMenu.positionViewAtIndex(mainMenu.currentIndex, ListView.End)
85− }
86− }
87−
88− // Don't load all the delegates (only max of 3 pages worth -1/0/+1)
89− cacheBuffer: Math.max(height * 3, units.gu(70))
90−
91− // Only allow flicking if the content doesn't fit on the page
92− interactive: contentHeight > height
93−
94− property int selectedIndex: -1
95− property bool blockCurrentIndexChange: false
96− // for count = 0
97− onCountChanged: {
98− if (count == 0 && selectedIndex != -1) {
99− selectedIndex = -1;
100− }
101− }
102− // for highlight following
103− onSelectedIndexChanged: {
104− if (currentIndex != selectedIndex) {
105− var blocked = blockCurrentIndexChange;
106− blockCurrentIndexChange = true;
107−
108− currentIndex = selectedIndex;
109−
110− blockCurrentIndexChange = blocked;
111− }
112− }
113− // for item addition/removal
114− onCurrentIndexChanged: {
115− if (!blockCurrentIndexChange) {
116− if (selectedIndex != -1 && selectedIndex != currentIndex) {
117− selectedIndex = currentIndex;
118− }
119− }
120− }
121−
122− Connections {
123− target: mainMenu.model ? mainMenu.model : null
124− onRowsAboutToBeRemoved: {
125− // track current item deletion.
126− if (mainMenu.selectedIndex >= first && mainMenu.selectedIndex <= last) {
127− mainMenu.selectedIndex = -1;
128− }
129− }
130− }
131−
132− delegate: Loader {
133− id: loader
134− objectName: "menuItem" + index
135− width: ListView.view.width
136− visible: status == Loader.Ready
137−
138− property int modelIndex: index
139− sourceComponent: factory.load(model, main.identifier)
140−
141− onLoaded: {
142− if (item.hasOwnProperty("selected")) {
143− item.selected = mainMenu.selectedIndex == index;
144− }
145− if (item.hasOwnProperty("menuSelected")) {
146− item.menuSelected.connect(function() { mainMenu.selectedIndex = index; });
147− }
148− if (item.hasOwnProperty("menuDeselected")) {
149− item.menuDeselected.connect(function() { mainMenu.selectedIndex = -1; });
150− }
151− if (item.hasOwnProperty("menuData")) {
152− item.menuData = Qt.binding(function() { return model; });
153− }
154− if (item.hasOwnProperty("menuIndex")) {
155− item.menuIndex = Qt.binding(function() { return modelIndex; });
156− }
157− }
158−
159− Binding {
160− target: item ? item : null
161− property: "objectName"
162− value: model.action
163− }
164−
165− // TODO: Fixes lp#1243146
166− // This is a workaround for a Qt bug. https://bugreports.qt-project.org/browse/QTBUG-34351
167− Connections {
168− target: mainMenu
169− onSelectedIndexChanged: {
170− if (loader.item && loader.item.hasOwnProperty("selected")) {
171− loader.item.selected = mainMenu.selectedIndex == index;
172− }
173− }
174− }
175− }
176− }
177−
178− MenuItemFactory {
179− id: _factory
180− rootModel: main.menuModel ? main.menuModel : null
181− menuModel: mainMenu.model ? mainMenu.model : null
182− }
183−
184− function reset()
185− {
186− mainMenu.positionViewAtBeginning();
187− }
188−}
qml/Panel/IndicatorItem.qml → qml/Panel/Indicators/IndicatorItem.qmlrenamed+2−1
@@ -18,7 +18,6 @@ import QtQuick 2.4
1818import Ubuntu.Components 1.3
1919import Ubuntu.Settings.Components 0.1
2020import QMenuModel 0.1
21−import "Indicators"
2221
2322IndicatorDelegate {
2423 id: root
@@ -157,6 +156,8 @@ IndicatorDelegate {
157156 }
158157
159158 StateGroup {
159+ objectName: "indicatorItemState"
160+
160161 states: [
161162 State {
162163 name: "minimised"
qml/Panel/Indicators/MenuItemFactory.qml → qml/Panel/Indicators/IndicatorMenuItemFactory.qmlrenamed+7−10
@@ -25,9 +25,10 @@ import Ubuntu.Components 1.3
2525import Unity.Session 0.1
2626import Unity.Platform 1.0
2727
28−Item {
28+Object {
2929 id: menuFactory
3030
31+ property string indicator
3132 property var rootModel: null
3233 property var menuModel: null
3334
@@ -88,7 +89,7 @@ Item {
8889 }
8990 }
9091
91− function getComponentForIndicatorEntryType(indicator, type) {
92+ function getComponentForIndicatorEntryType(type) {
9293 var component = undefined;
9394 var map = _userMap || _typeToComponent
9495 var indicatorComponents = map[indicator];
@@ -112,7 +113,7 @@ Item {
112113 return component
113114 }
114115
115− function getComponentForIndicatorEntryAction(indicator, action) {
116+ function getComponentForIndicatorEntryAction(action) {
116117 var component = undefined;
117118 var indicatorFilter = _action_filter_map[indicator]
118119
@@ -1082,17 +1083,13 @@ Item {
10821083 }
10831084 }
10841085
1085− function load(modelData, context) {
1086− if (context && context.indexOf("fake-") == 0) {
1087− context = context.substring("fake-".length)
1088− }
1089−
1090− var component = getComponentForIndicatorEntryAction(context, modelData.action)
1086+ function load(modelData) {
1087+ var component = getComponentForIndicatorEntryAction(modelData.action)
10911088 if (component !== undefined) {
10921089 return component
10931090 }
10941091
1095− component = getComponentForIndicatorEntryType(context, modelData.type)
1092+ component = getComponentForIndicatorEntryType(modelData.type)
10961093 if (component !== undefined) {
10971094 return component;
10981095 }
qml/Panel/MenuContent.qml+7−10
@@ -25,8 +25,10 @@ import "Indicators"
2525Rectangle {
2626 id: content
2727
28− property QtObject indicatorsModel: null
28+ property QtObject model: null
2929 property int currentMenuIndex: -1
30+ property Component pageDelegate
31+
3032 color: theme.palette.normal.background
3133
3234 width: units.gu(40)
@@ -40,7 +42,7 @@ Rectangle {
4042 id: listViewContent
4143 objectName: "indicatorsContentListView"
4244 anchors.fill: parent
43− model: content.indicatorsModel
45+ model: content.model
4446
4547 highlightFollowsCurrentItem: true
4648 interactive: false
@@ -59,18 +61,13 @@ Rectangle {
5961
6062 width: ListView.view.width
6163 height: ListView.view.height
62− objectName: identifier
6364 asynchronous: true
6465 visible: ListView.isCurrentItem
6566
66− sourceComponent: IndicatorPage {
67− objectName: identifier + "-page"
67+ property var modelData: model
68+ property var modelIndex: index
6869
69− identifier: model.identifier
70− busName: indicatorProperties.busName
71− actionsObjectPath: indicatorProperties.actionsObjectPath
72− menuObjectPath: indicatorProperties.menuObjectPath
73− }
70+ sourceComponent: pageDelegate
7471
7572 onVisibleChanged: {
7673 // Reset the indicator states
qml/Panel/Panel.qml+300−96
@@ -16,91 +16,160 @@
1616
1717import QtQuick 2.4
1818import Ubuntu.Components 1.3
19+import Ubuntu.Layouts 1.0
1920import Unity.Application 0.1
21+import Unity.Indicators 0.1
22+import Utils 0.1
23+import Unity.ApplicationMenu 0.1
24+
25+import QtQuick.Window 2.2
26+// for indicator-keyboard
27+import AccountsService 0.1
28+import Unity.InputInfo 0.1
29+
30+import "../ApplicationMenus"
2031import "../Components"
2132import "../Components/PanelState"
2233import ".."
34+import "Indicators"
2335
2436Item {
2537 id: root
26− readonly property real panelHeight: indicatorArea.y + d.indicatorHeight
38+ readonly property real panelHeight: panelArea.y + minimizedPanelHeight
39+
40+ property real minimizedPanelHeight: units.gu(3)
41+ property real expandedPanelHeight: units.gu(7)
42+ property real indicatorMenuWidth: width
43+ property real applicationMenuWidth: width
44+
45+ property alias applicationMenus: __applicationMenus
2746 property alias indicators: __indicators
28− property alias callHint: __callHint
2947 property bool fullscreenMode: false
30− property real indicatorAreaShowProgress: 1.0
31− property bool locked: false
48+ property real panelAreaShowProgress: 1.0
49+ property bool greeterShown: false
50+
51+ property string mode: "staged"
3252
3353 MouseArea {
54+ id: backMouseEater
3455 anchors.fill: parent
3556 anchors.topMargin: panelHeight
36− visible: !indicators.fullyClosed
57+ visible: !indicators.fullyClosed || !applicationMenus.fullyClosed
3758 enabled: visible
38− onClicked: if (indicators.fullyOpened) indicators.hide();
3959 hoverEnabled: true // should also eat hover events, otherwise they will pass through
60+
61+ onClicked: {
62+ __applicationMenus.hide();
63+ __indicators.hide();
64+ }
4065 }
4166
4267 Binding {
4368 target: PanelState
4469 property: "panelHeight"
45− value: indicators.minimizedPanelHeight
70+ value: minimizedPanelHeight
71+ }
72+
73+ RegisteredApplicationMenuModel {
74+ id: registeredMenuModel
75+ persistentSurfaceId: PanelState.focusedPersistentSurfaceId
76+ }
77+
78+ QtObject {
79+ id: d
80+
81+ property bool revealControls: !greeterShown &&
82+ !applicationMenus.shown &&
83+ !indicators.shown &&
84+ (decorationMouseArea.containsMouse || menuBarLoader.menusRequested)
85+
86+ property bool showWindowDecorationControls: (revealControls && PanelState.decorationsVisible) ||
87+ PanelState.decorationsAlwaysVisible
88+
89+ property bool showPointerMenu: revealControls &&
90+ (PanelState.decorationsVisible || mode == "staged")
91+
92+ property bool enablePointerMenu: revealControls &&
93+ applicationMenus.available &&
94+ applicationMenus.model
95+
96+ property bool showTouchMenu: !greeterShown &&
97+ !showPointerMenu
98+
99+ property bool enableTouchMenus: showTouchMenu &&
100+ applicationMenus.available &&
101+ applicationMenus.model
46102 }
47103
48104 Item {
49− id: indicatorArea
50− objectName: "indicatorArea"
105+ id: panelArea
106+ objectName: "panelArea"
51107
52108 anchors.fill: parent
53109
54110 transform: Translate {
55111 y: indicators.state === "initial"
56− ? (1.0 - indicatorAreaShowProgress) * -d.indicatorHeight
112+ ? (1.0 - panelAreaShowProgress) * - minimizedPanelHeight
57113 : 0
58114 }
59115
60116 BorderImage {
61117 id: indicatorsDropShadow
62118 anchors {
63− fill: indicators
64− leftMargin: -units.gu(1)
65− bottomMargin: -units.gu(1)
119+ fill: __indicators
120+ margins: -units.gu(1)
121+ }
122+ visible: !__indicators.fullyClosed
123+ source: "graphics/rectangular_dropshadow.sci"
124+ }
125+
126+ BorderImage {
127+ id: appmenuDropShadow
128+ anchors {
129+ fill: __applicationMenus
130+ margins: -units.gu(1)
66131 }
67− visible: !indicators.fullyClosed
132+ visible: !__applicationMenus.fullyClosed
68133 source: "graphics/rectangular_dropshadow.sci"
69134 }
70135
71136 BorderImage {
72137 id: panelDropShadow
73138 anchors {
74− fill: indicatorAreaBackground
139+ fill: panelAreaBackground
75140 bottomMargin: -units.gu(1)
76141 }
77− visible: PanelState.dropShadow && !callHint.visible
142+ visible: PanelState.dropShadow
78143 source: "graphics/rectangular_dropshadow.sci"
79144 }
80145
81146 Rectangle {
82− id: indicatorAreaBackground
147+ id: panelAreaBackground
83148 color: callHint.visible ? theme.palette.normal.positive : theme.palette.normal.background
84149 anchors {
85150 top: parent.top
86151 left: parent.left
87152 right: parent.right
88153 }
89− height: indicators.minimizedPanelHeight
154+ height: minimizedPanelHeight
90155
91156 Behavior on color { ColorAnimation { duration: UbuntuAnimation.FastDuration } }
92157 }
93158
94159 MouseArea {
160+ id: decorationMouseArea
95161 objectName: "windowControlArea"
96162 anchors {
97− top: parent.top
98163 left: parent.left
99− right: indicators.left
164+ right: parent.right
165+ }
166+ height: minimizedPanelHeight
167+ hoverEnabled: !__indicators.shown
168+ onClicked: {
169+ if (callHint.visible) {
170+ callHint.showLiveCall();
171+ }
100172 }
101− height: indicators.minimizedPanelHeight
102− hoverEnabled: true
103− onClicked: if (callHint.visible) { callHint.showLiveCall(); }
104173
105174 onPressed: {
106175 if (!callHint.visible) {
@@ -109,29 +178,135 @@ Item {
109178 }
110179 }
111180
112− // WindowControlButtons inside the mouse area, otherwise QML doesn't grok nested hover events :/
113− // cf. https://bugreports.qt.io/browse/QTBUG-32909
114− WindowControlButtons {
115− id: windowControlButtons
116− objectName: "panelWindowControlButtons"
117− anchors {
118− left: parent.left
119− top: parent.top
181+ Row {
182+ anchors.fill: parent
183+ spacing: units.gu(2)
184+
185+ // WindowControlButtons inside the mouse area, otherwise QML doesn't grok nested hover events :/
186+ // cf. https://bugreports.qt.io/browse/QTBUG-32909
187+ WindowControlButtons {
188+ id: windowControlButtons
189+ objectName: "panelWindowControlButtons"
190+ height: indicators.minimizedPanelHeight
191+ opacity: d.showWindowDecorationControls ? 1 : 0
192+ visible: opacity != 0
193+ Behavior on opacity { UbuntuNumberAnimation { duration: UbuntuAnimation.SnapDuration } }
194+
195+ active: PanelState.decorationsVisible || PanelState.decorationsAlwaysVisible
196+ windowIsMaximized: true
197+ onCloseClicked: PanelState.closeClicked()
198+ onMinimizeClicked: PanelState.minimizeClicked()
199+ onMaximizeClicked: PanelState.restoreClicked()
200+ closeButtonShown: PanelState.closeButtonShown
201+ }
202+
203+ Loader {
204+ id: menuBarLoader
205+ height: parent.height
206+ enabled: d.enablePointerMenu
207+ opacity: d.showPointerMenu ? 1 : 0
208+ visible: opacity != 0
209+ Behavior on opacity { UbuntuNumberAnimation { duration: UbuntuAnimation.SnapDuration } }
210+ active: __applicationMenus.model
211+
212+ property bool menusRequested: menuBarLoader.item ? menuBarLoader.item.showRequested : false
213+
214+ sourceComponent: MenuBar {
215+ id: bar
216+ objectName: "menuBar"
217+ anchors.left: parent.left
218+ anchors.margins: units.gu(1)
219+ height: menuBarLoader.height
220+ enableKeyFilter: valid && PanelState.decorationsVisible
221+ unityMenuModel: __applicationMenus.model
222+
223+ Connections {
224+ target: __applicationMenus
225+ onShownChanged: bar.dismiss();
226+ }
227+
228+ Connections {
229+ target: __indicators
230+ onShownChanged: bar.dismiss();
231+ }
232+ }
233+ }
234+ }
235+
236+ ActiveCallHint {
237+ id: callHint
238+ objectName: "callHint"
239+
240+ anchors.centerIn: parent
241+ height: minimizedPanelHeight
242+
243+ visible: active && indicators.state == "initial" && __applicationMenus.state == "initial"
244+ greeterShown: root.greeterShown
245+ }
246+ }
247+
248+ PanelMenu {
249+ id: __applicationMenus
250+
251+ model: registeredMenuModel.model
252+ width: root.applicationMenuWidth
253+ minimizedPanelHeight: root.minimizedPanelHeight
254+ expandedPanelHeight: root.expandedPanelHeight
255+ openedHeight: root.height
256+ alignment: Qt.AlignLeft
257+ enableHint: !callHint.active && !fullscreenMode
258+ showOnClick: false
259+ panelColor: panelAreaBackground.color
260+
261+ onShowTapped: {
262+ if (callHint.active) {
263+ callHint.showLiveCall();
264+ }
265+ }
266+
267+ showRowTitle: !expanded
268+ rowTitle: PanelState.title
269+ rowItemDelegate: ActionItem {
270+ id: actionItem
271+ property int ownIndex: index
272+ objectName: "appMenuItem"+index
273+
274+ width: _title.width + units.gu(2)
275+ height: parent.height
276+
277+ action: Action {
278+ text: model.label.replace("_", "&")
279+ }
280+
281+ Label {
282+ id: _title
283+ anchors.centerIn: parent
284+ text: actionItem.text
285+ horizontalAlignment: Text.AlignLeft
286+ color: enabled ? "white" : "#5d5d5d"
287+ }
288+ }
289+
290+ pageDelegate: PanelMenuPage {
291+ menuModel: __applicationMenus.model
292+ submenuIndex: modelIndex
293+
294+ factory: ApplicationMenuItemFactory {
295+ rootModel: __applicationMenus.model
120296 }
121− height: indicators.minimizedPanelHeight
297+ }
298+
299+ enabled: d.enableTouchMenus
300+ opacity: d.showTouchMenu ? 1 : 0
301+ visible: opacity != 0
302+ Behavior on opacity { UbuntuNumberAnimation { duration: UbuntuAnimation.SnapDuration } }
122303
123− visible: ((PanelState.buttonsVisible && parent.containsMouse) || PanelState.buttonsAlwaysVisible)
124− && !root.locked && !callHint.visible
125− active: PanelState.buttonsVisible || PanelState.buttonsAlwaysVisible
126− windowIsMaximized: true
127− onCloseClicked: PanelState.closeClicked()
128− onMinimizeClicked: PanelState.minimizeClicked()
129− onMaximizeClicked: PanelState.restoreClicked()
130− closeButtonShown: PanelState.closeButtonShown
304+ onEnabledChanged: {
305+ if (!enabled) hide();
131306 }
132307 }
133308
134− IndicatorsMenu {
309+ PanelMenu {
135310 id: __indicators
136311 objectName: "indicators"
137312
@@ -139,72 +314,93 @@ Item {
139314 top: parent.top
140315 right: parent.right
141316 }
142−
143− shown: false
144− width: root.width - (windowControlButtons.visible ? windowControlButtons.width + titleLabel.width : 0)
145− minimizedPanelHeight: units.gu(3)
146− expandedPanelHeight: units.gu(7)
317+ width: root.indicatorMenuWidth
318+ minimizedPanelHeight: root.minimizedPanelHeight
319+ expandedPanelHeight: root.expandedPanelHeight
147320 openedHeight: root.height
148321
149− overFlowWidth: {
150− if (callHint.visible) {
151− return Math.max(root.width - (callHint.width + units.gu(2)), 0)
152− }
153− return root.width
154− }
322+ overFlowWidth: root.width
155323 enableHint: !callHint.active && !fullscreenMode
156324 showOnClick: !callHint.visible
157− panelColor: indicatorAreaBackground.color
325+ panelColor: panelAreaBackground.color
158326
159327 onShowTapped: {
160328 if (callHint.active) {
161329 callHint.showLiveCall();
162330 }
163331 }
164− }
165332
166− Label {
167− id: titleLabel
168− objectName: "windowDecorationTitle"
169− anchors {
170− left: parent.left
171− right: __indicators.left
172− top: parent.top
173− leftMargin: units.gu(1)
174− rightMargin: units.gu(1)
175− topMargin: units.gu(0.5)
176− bottomMargin: units.gu(0.5)
177− }
178− color: "white"
179− height: indicators.minimizedPanelHeight - anchors.topMargin - anchors.bottomMargin
180− opacity: !windowControlButtons.visible && !root.locked && !callHint.visible ? 1 : 0
181− visible: opacity != 0
182− verticalAlignment: Text.AlignVCenter
183− fontSize: "medium"
184− font.weight: PanelState.buttonsVisible ? Font.Light : Font.Medium
185− text: PanelState.title
186− elide: Text.ElideRight
187− maximumLineCount: 1
188− Behavior on opacity { UbuntuNumberAnimation {} }
189− }
333+ rowItemDelegate: IndicatorItem {
334+ id: indicatorItem
335+ objectName: identifier+"-panelItem"
190336
191− // TODO here would the Locally integrated menus come
337+ property int ownIndex: index
338+ property bool overflow: parent.width - x > __indicators.overFlowWidth
339+ property bool hidden: !expanded && (overflow || !indicatorVisible || hideSessionIndicator || hideKeyboardIndicator)
340+ // HACK for indicator-session
341+ readonly property bool hideSessionIndicator: identifier == "indicator-session" && Math.min(Screen.width, Screen.height) <= units.gu(60)
342+ // HACK for indicator-keyboard
343+ readonly property bool hideKeyboardIndicator: identifier == "indicator-keyboard" && (AccountsService.keymaps.length < 2 || keyboardsModel.count == 0)
192344
193− ActiveCallHint {
194− id: __callHint
195− anchors {
196− top: parent.top
197− left: parent.left
345+ height: parent.height
346+ expanded: indicators.expanded
347+ selected: ListView.isCurrentItem
348+
349+ identifier: model.identifier
350+ busName: indicatorProperties.busName
351+ actionsObjectPath: indicatorProperties.actionsObjectPath
352+ menuObjectPath: indicatorProperties.menuObjectPath
353+
354+ opacity: hidden ? 0.0 : 1.0
355+ Behavior on opacity { UbuntuNumberAnimation { duration: UbuntuAnimation.SnapDuration } }
356+
357+ width: ((expanded || indicatorVisible) && !hideSessionIndicator && !hideKeyboardIndicator) ? implicitWidth : 0
358+
359+ Behavior on width { UbuntuNumberAnimation { duration: UbuntuAnimation.SnapDuration } }
360+ }
361+
362+ pageDelegate: PanelMenuPage {
363+ objectName: modelData.identifier + "-page"
364+ submenuIndex: 0
365+
366+ menuModel: delegate.menuModel
367+
368+ factory: IndicatorMenuItemFactory {
369+ indicator: {
370+ var context = modelData.identifier;
371+ if (context && context.indexOf("fake-") === 0) {
372+ context = context.substring("fake-".length)
373+ }
374+ return context;
375+ }
376+ rootModel: delegate.menuModel
377+ }
378+
379+ IndicatorDelegate {
380+ id: delegate
381+ busName: modelData.indicatorProperties.busName
382+ actionsObjectPath: modelData.indicatorProperties.actionsObjectPath
383+ menuObjectPath: modelData.indicatorProperties.menuObjectPath
384+ }
385+ }
386+
387+ enabled: !applicationMenus.expanded
388+ opacity: !applicationMenus.expanded ? 1 : 0
389+ Behavior on opacity { UbuntuNumberAnimation { duration: UbuntuAnimation.SnapDuration } }
390+
391+ onEnabledChanged: {
392+ if (!enabled) hide();
198393 }
199− height: indicators.minimizedPanelHeight
200− visible: active && indicators.state == "initial"
201394 }
202395 }
203396
204− QtObject {
205− id: d
206− objectName: "panelPriv"
207− readonly property real indicatorHeight: indicators.minimizedPanelHeight
397+ InputDeviceModel {
398+ id: keyboardsModel
399+ deviceFilter: InputInfo.Keyboard
400+ }
401+
402+ IndicatorsLight {
403+ id: indicatorLights
208404 }
209405
210406 states: [
@@ -212,7 +408,7 @@ Item {
212408 name: "onscreen" //fully opaque and visible at top edge of screen
213409 when: !fullscreenMode
214410 PropertyChanges {
215− target: indicatorArea;
411+ target: panelArea;
216412 anchors.topMargin: 0
217413 opacity: 1;
218414 }
@@ -221,25 +417,33 @@ Item {
221417 name: "offscreen" //pushed off screen
222418 when: fullscreenMode
223419 PropertyChanges {
224− target: indicatorArea;
225− anchors.topMargin: indicators.state === "initial" ? -d.indicatorHeight : 0
226− opacity: indicators.fullyClosed ? 0.0 : 1.0
420+ target: panelArea;
421+ anchors.topMargin: {
422+ if (indicators.state !== "initial") return 0;
423+ if (applicationMenus.state !== "initial") return 0;
424+ return -minimizedPanelHeight;
425+ }
426+ opacity: indicators.fullyClosed && applicationMenus.fullyClosed ? 0.0 : 1.0
227427 }
228428 PropertyChanges {
229429 target: indicators.showDragHandle;
230430 anchors.bottomMargin: -units.gu(1)
231431 }
432+ PropertyChanges {
433+ target: applicationMenus.showDragHandle;
434+ anchors.bottomMargin: -units.gu(1)
435+ }
232436 }
233437 ]
234438
235439 transitions: [
236440 Transition {
237441 to: "onscreen"
238− UbuntuNumberAnimation { target: indicatorArea; properties: "anchors.topMargin,opacity" }
442+ UbuntuNumberAnimation { target: panelArea; properties: "anchors.topMargin,opacity" }
239443 },
240444 Transition {
241445 to: "offscreen"
242− UbuntuNumberAnimation { target: indicatorArea; properties: "anchors.topMargin,opacity" }
446+ UbuntuNumberAnimation { target: panelArea; properties: "anchors.topMargin,opacity" }
243447 }
244448 ]
245449}
qml/Panel/IndicatorsBar.qml → qml/Panel/PanelBar.qmlrenamed+69−36
@@ -22,13 +22,19 @@ Item {
2222 id: root
2323 property alias expanded: row.expanded
2424 property alias interactive: flickable.interactive
25− property alias indicatorsModel: row.indicatorsModel
25+ property alias model: row.model
2626 property alias unitProgress: row.unitProgress
2727 property alias enableLateralChanges: row.enableLateralChanges
2828 property alias overFlowWidth: row.overFlowWidth
2929 readonly property alias currentItemIndex: row.currentItemIndex
3030 property real lateralPosition: -1
31− readonly property string currentIndicator: row.currentItem ? row.currentItem.identifier : ""
31+ property int alignment: Qt.AlignRight
32+
33+ property alias showRowTitle: row.showRowTitle
34+ property alias rowTitle: row.rowTitle
35+ property alias rowItemDelegate: row.delegate
36+
37+ implicitWidth: flickable.contentWidth
3238
3339 function selectItemAt(lateralPosition) {
3440 if (!expanded) {
@@ -46,15 +52,21 @@ Item {
4652 }
4753
4854 function addScrollOffset(scrollAmmout) {
55+ if (root.alignment == Qt.AlignLeft) {
56+ scrollAmmout = -scrollAmmout;
57+ }
58+
4959 if (scrollAmmout < 0) { // left scroll
5060 if (flickable.contentX + flickable.width > row.width) return; // already off the left.
5161
52− if (flickable.contentX + flickable.width - scrollAmmout > row.width) { // going to be off the right
62+ if (flickable.contentX + flickable.width - scrollAmmout > row.width) { // going to be off the left
5363 scrollAmmout = (flickable.contentX + flickable.width) - row.width;
5464 }
5565 } else { // right scroll
5666 if (flickable.contentX < 0) return; // already off the right.
57− if (flickable.contentX - scrollAmmout < 0) scrollAmmout = flickable.contentX; // going to be off the right
67+ if (flickable.contentX - scrollAmmout < 0) { // going to be off the right
68+ scrollAmmout = flickable.contentX;
69+ }
5870 }
5971 d.scrollOffset = d.scrollOffset + scrollAmmout;
6072 }
@@ -62,14 +74,19 @@ Item {
6274 QtObject {
6375 id: d
6476 property var initialItem
65− // the non-expanded distance from row offset to center of initial item
66− property real originalDistanceFromRight: -1
77+ // the non-expanded distance from alignment edge to center of initial item
78+ property real originalDistanceFromEdge: -1
6779
68− // calculate the distance from row offset to center of initial item
69− property real distanceFromRight: {
70− if (originalDistanceFromRight == -1) return 0;
80+ // calculate the distance from row alignment edge edge to center of initial item
81+ property real distanceFromEdge: {
82+ if (originalDistanceFromEdge == -1) return 0;
7183 if (!initialItem) return 0;
72− return row.width - initialItem.x - initialItem.width /2;
84+
85+ if (root.alignment == Qt.AlignLeft) {
86+ return initialItem.x - initialItem.width / 2;
87+ } else {
88+ return row.width - initialItem.x - initialItem.width / 2;
89+ }
7390 }
7491
7592 // offset to the intially selected expanded item
@@ -82,7 +99,11 @@ Item {
8299 onScrollOffsetChanged: root.lateralPositionChanged()
83100
84101 onInitialItemChanged: {
85− originalDistanceFromRight = initialItem ? (row.width - initialItem.x - initialItem.width/2) : -1;
102+ if (root.alignment == Qt.AlignLeft) {
103+ originalDistanceFromEdge = initialItem ? (initialItem.x - initialItem.width/2) : -1;
104+ } else {
105+ originalDistanceFromEdge = initialItem ? (row.width - initialItem.x - initialItem.width/2) : -1;
106+ }
86107 }
87108
88109 Behavior on alignmentAdjustment {
@@ -93,26 +114,38 @@ Item {
93114 flickable.resetContentXComponents();
94115
95116 if (expanded && !flickable.moving) {
96− // gap between left and row?
97− if (flickable.contentX + flickable.width > row.width) {
98− // row width is less than flickable
99− if (row.width < flickable.width) {
100− d.alignmentAdjustment -= flickable.contentX;
101− } else {
102− d.alignmentAdjustment -= ((flickable.contentX + flickable.width) - row.width);
103− }
104117
105− // gap between right and row?
106− } else if (flickable.contentX < 0) {
107− d.alignmentAdjustment -= flickable.contentX;
118+ if (root.alignment == Qt.AlignLeft) {
119+ // current item overlap on left
120+ if (row.currentItem && flickable.contentX > row.currentItem.x) {
121+ d.alignmentAdjustment -= (flickable.contentX - row.currentItem.x);
122+
123+ // current item overlap on right
124+ } else if (row.currentItem && flickable.contentX + flickable.width < row.currentItem.x + row.currentItem.width) {
125+ d.alignmentAdjustment += (row.currentItem.x + row.currentItem.width) - (flickable.contentX + flickable.width);
126+ }
127+ } else {
128+ // gap between left and row?
129+ if (flickable.contentX + flickable.width > row.width) {
130+ // row width is less than flickable
131+ if (row.width < flickable.width) {
132+ d.alignmentAdjustment -= flickable.contentX;
133+ } else {
134+ d.alignmentAdjustment -= ((flickable.contentX + flickable.width) - row.width);
135+ }
136+
137+ // gap between right and row?
138+ } else if (flickable.contentX < 0) {
139+ d.alignmentAdjustment -= flickable.contentX;
108140
109− // current item overlap on left
110− } else if (row.currentItem && (flickable.contentX + flickable.width) < (row.width - row.currentItem.x)) {
111− d.alignmentAdjustment += ((row.width - row.currentItem.x) - (flickable.contentX + flickable.width));
141+ // current item overlap on left
142+ } else if (row.currentItem && (flickable.contentX + flickable.width) < (row.width - row.currentItem.x)) {
143+ d.alignmentAdjustment += ((row.width - row.currentItem.x) - (flickable.contentX + flickable.width));
112144
113− // current item overlap on right
114− } else if (row.currentItem && flickable.contentX > (row.width - row.currentItem.x - row.currentItem.width)) {
115− d.alignmentAdjustment -= flickable.contentX - (row.width - row.currentItem.x - row.currentItem.width);
145+ // current item overlap on right
146+ } else if (row.currentItem && flickable.contentX > (row.width - row.currentItem.x - row.currentItem.width)) {
147+ d.alignmentAdjustment -= flickable.contentX - (row.width - row.currentItem.x - row.currentItem.width);
148+ }
116149 }
117150 }
118151 }
@@ -140,7 +173,7 @@ Item {
140173
141174 // we rotate it because we want the Flickable to align its content item
142175 // on the right instead of on the left
143− rotation: 180
176+ rotation: root.alignment != Qt.AlignRight ? 0 : 180
144177
145178 anchors.fill: parent
146179 contentWidth: row.width
@@ -161,16 +194,16 @@ Item {
161194 }
162195 }
163196
164− IndicatorItemRow {
197+ PanelItemRow {
165198 id: row
166− objectName: "indicatorItemRow"
199+ objectName: "panelItemRow"
167200 anchors {
168201 top: parent.top
169202 bottom: parent.bottom
170203 }
171204
172205 // Compensate for the Flickable rotation (ie, counter-rotate)
173− rotation: 180
206+ rotation: root.alignment != Qt.AlignRight ? 0 : 180
174207
175208 lateralPosition: {
176209 if (root.lateralPosition == -1) return -1;
@@ -230,15 +263,15 @@ Item {
230263 target: d
231264 rowOffset: {
232265 if (!initialItem) return 0;
233− if (distanceFromRight - initialItem.width <= 0) return 0;
266+ if (distanceFromEdge - initialItem.width <= 0) return 0;
234267
235− var rowOffset = distanceFromRight - originalDistanceFromRight;
268+ var rowOffset = distanceFromEdge - originalDistanceFromEdge;
236269 return rowOffset;
237270 }
238271 restoreEntryValues: false
239272 }
240− }
241− , State {
273+ },
274+ State {
242275 name: "interactive"
243276 when: expanded && interactive
244277
qml/Panel/IndicatorItemRow.qml → qml/Panel/PanelItemRow.qmlrenamed+51−100
@@ -15,29 +15,29 @@
1515 */
1616
1717import QtQuick 2.4
18−import QtQuick.Window 2.2
1918import Ubuntu.Components 1.3
20−
21−// for indicator-keyboard
22−import AccountsService 0.1
23−import Unity.InputInfo 0.1
19+import "../Components"
2420
2521Item {
2622 id: root
27− width: row.width
28− height: units.gu(3)
23+ implicitWidth: showRowTitle && !expanded ? rowTitle != "" ? rowLabel.width : 0 : row.width
24+ implicitHeight: units.gu(3)
2925
30− property QtObject indicatorsModel: null
26+ property bool showRowTitle: false
27+ property alias rowTitle: rowLabel.text
28+ property QtObject model: null
3129 property real overFlowWidth: width
3230 property bool expanded: false
33− property var currentItem
34− readonly property int currentItemIndex: currentItem ? currentItem.ownIndex : -1
31+ readonly property alias currentItem: row.currentItem
32+ readonly property alias currentItemIndex: row.currentIndex
3533
3634 property real unitProgress: 0.0
3735 property real selectionChangeBuffer: units.gu(2)
3836 property bool enableLateralChanges: false
3937 property color hightlightColor: "#ffffff"
4038
39+ property alias delegate: row.delegate
40+
4141 property real lateralPosition: -1
4242 onLateralPositionChanged: {
4343 updateItemFromLateralPosition();
@@ -86,21 +86,21 @@ Item {
8686 }
8787
8888 function indicatorAt(x, y) {
89− var item = row.childAt(x, y);
89+ var item = row.itemAt(x, y);
9090 return item && item.hasOwnProperty("ownIndex") ? item : null;
9191 }
9292
9393 function resetCurrentItem() {
9494 d.firstItemSwitch = true;
95− d.previousItem = undefined;
96− currentItem = undefined;
95+ d.previousItem = null;
96+ row.currentIndex = -1;
9797 }
9898
9999 function setCurrentItemIndex(index) {
100− for (var i = 0; i < row.children.length; i++) {
101− var item = row.children[i];
100+ for (var i = 0; i < row.contentItem.children.length; i++) {
101+ var item = row.contentItem.children[i];
102102 if (item.hasOwnProperty("ownIndex") && item.ownIndex === index) {
103− if (currentItem !== item) currentItem = item;
103+ if (currentItem !== item) row.currentIndex = index;
104104 break;
105105 }
106106 }
@@ -109,18 +109,18 @@ Item {
109109 function selectItemAt(lateralPosition) {
110110 var item = indicatorAt(lateralPosition, 0);
111111 if (item && item.opacity > 0) {
112− currentItem = item;
112+ row.currentIndex = item.ownIndex;
113113 } else {
114114 // Select default item.
115− var searchIndex = lateralPosition > width ? repeater.count - 1 : 0;
115+ var searchIndex = lateralPosition >= width ? row.count - 1 : 0;
116116
117− for (var i = 0; i < row.children.length; i++) {
118− if (row.children[i].hasOwnProperty("ownIndex") && row.children[i].ownIndex === searchIndex) {
119− item = row.children[i];
117+ for (var i = 0; i < row.contentItem.children.length; i++) {
118+ if (row.contentItem.children[i].hasOwnProperty("ownIndex") && row.contentItem.children[i].ownIndex === searchIndex) {
119+ item = row.contentItem.children[i];
120120 break;
121121 }
122122 }
123− if (currentItem !== item) currentItem = item;
123+ if (currentItem !== item) row.currentIndex = item ? item.ownIndex : -1;
124124 }
125125 }
126126
@@ -131,11 +131,6 @@ Item {
131131 property bool forceAlignmentAnimationDisabled: false
132132 }
133133
134− InputDeviceModel {
135− id: keyboardsModel
136− deviceFilter: InputInfo.Keyboard
137− }
138−
139134 onCurrentItemChanged: {
140135 if (d.previousItem) {
141136 d.firstItemSwitch = false;
@@ -143,8 +138,32 @@ Item {
143138 d.previousItem = currentItem;
144139 }
145140
146− Row {
141+ Label {
142+ id: rowLabel
143+ objectName: "panelTitle"
144+ anchors {
145+ left: parent.left
146+ leftMargin: units.gu(1)
147+ verticalCenter: parent.verticalCenter
148+ }
149+ width: implicitWidth + units.gu(2)
150+ elide: Text.ElideRight
151+ maximumLineCount: 1
152+ fontSize: "medium"
153+ font.weight: Font.Medium
154+ color: Theme.palette.selected.backgroundText
155+ opacity: showRowTitle ? 1 : 0
156+ visible: opacity != 0
157+ Behavior on opacity { NumberAnimation { duration: UbuntuAnimation.SnapDuration } }
158+ }
159+
160+ ListView {
147161 id: row
162+ objectName: "panelRow"
163+ orientation: ListView.Horizontal
164+ model: root.model
165+ opacity: showRowTitle ? 0 : 1
166+ // dont set visible on basis of opacity; otherwise width will not be calculated correctly
148167 anchors {
149168 top: parent.top
150169 bottom: parent.bottom
@@ -153,7 +172,7 @@ Item {
153172 // TODO: make this better
154173 // when the width changes, the highlight will lag behind due to animation, so we need to disable the animation
155174 // and adjust the highlight X immediately.
156− width: implicitWidth
175+ width: childrenRect.width
157176 Behavior on width {
158177 SequentialAnimation {
159178 ScriptAction {
@@ -166,75 +185,7 @@ Item {
166185 }
167186 }
168187
169− Repeater {
170− id: repeater
171− model: indicatorsModel
172− visible: false
173−
174− onItemRemoved: {
175− // current item removed.
176− if (currentItem === item) {
177− var i = 0;
178− while (i < row.children.length) {
179− var childItem = row.children[i];
180− if (childItem !== item) {
181− setCurrentItemIndex(i);
182− break;
183− }
184− i++;
185− }
186− }
187− }
188−
189−
190− delegate: IndicatorItem {
191− id: indicatorItem
192− objectName: identifier+"-panelItem"
193−
194− property int ownIndex: index
195− property bool overflow: row.width - x > overFlowWidth
196− property bool hidden: !expanded && (overflow || !indicatorVisible || hideSessionIndicator || hideKeyboardIndicator)
197− // HACK for indicator-session
198− readonly property bool hideSessionIndicator: identifier == "indicator-session" && Math.min(Screen.width, Screen.height) <= units.gu(60)
199− // HACK for indicator-keyboard
200− readonly property bool hideKeyboardIndicator: identifier == "indicator-keyboard" && (AccountsService.keymaps.length < 2 || keyboardsModel.count == 0)
201−
202− height: row.height
203− expanded: root.expanded
204− selected: currentItem === this
205−
206− identifier: model.identifier
207− busName: indicatorProperties.busName
208− actionsObjectPath: indicatorProperties.actionsObjectPath
209− menuObjectPath: indicatorProperties.menuObjectPath
210−
211− opacity: hidden ? 0.0 : 1.0
212− Behavior on opacity {
213− NumberAnimation { duration: UbuntuAnimation.SnapDuration; easing: UbuntuAnimation.StandardEasing }
214− }
215−
216− width: ((expanded || indicatorVisible) && !hideSessionIndicator && !hideKeyboardIndicator) ? implicitWidth : 0
217−
218− Behavior on width {
219− NumberAnimation { duration: UbuntuAnimation.SnapDuration; easing: UbuntuAnimation.StandardEasing }
220− }
221−
222− Component.onDestruction: {
223− // current item removed.
224− if (currentItem === this) {
225− var i = 0;
226− while (i < row.children.length) {
227− var childItem = row.children[i];
228− if (childItem !== this) {
229− setCurrentItemIndex(i);
230− break;
231− }
232− i++;
233− }
234− }
235− }
236− }
237− }
188+ Behavior on opacity { NumberAnimation { duration: UbuntuAnimation.SnapDuration } }
238189 }
239190
240191 Rectangle {
@@ -244,7 +195,7 @@ Item {
244195 anchors.bottom: row.bottom
245196 height: units.dp(2)
246197 color: root.hightlightColor
247− visible: currentItem !== undefined
198+ visible: currentItem !== null
248199 opacity: 0.0
249200
250201 width: currentItem ? currentItem.width : 0
@@ -269,7 +220,7 @@ Item {
269220
270221 if (currentItem && currentItem.ownIndex === 0 && distanceFromCenter < 0) {
271222 return 0;
272− } else if (currentItem && currentItem.ownIndex === repeater.count-1 & distanceFromCenter > 0) {
223+ } else if (currentItem && currentItem.ownIndex === row.count-1 & distanceFromCenter > 0) {
273224 return 0;
274225 }
275226 return (distanceFromCenter / (currentItem.width / 4)) * units.gu(1);
qml/Panel/IndicatorsMenu.qml → qml/Panel/PanelMenu.qmlrenamed+31−19
@@ -22,22 +22,30 @@ import "Indicators"
2222
2323Showable {
2424 id: root
25− property alias indicatorsModel: bar.indicatorsModel
25+ property alias model: bar.model
2626 property alias showDragHandle: __showDragHandle
2727 property alias hideDragHandle: __hideDragHandle
2828 property alias overFlowWidth: bar.overFlowWidth
2929 property alias verticalVelocityThreshold: yVelocityCalculator.velocityThreshold
30− property alias currentIndicator: bar.currentIndicator
3130 property int minimizedPanelHeight: units.gu(3)
3231 property int expandedPanelHeight: units.gu(7)
3332 property real openedHeight: units.gu(71)
33+ property bool enableHint: true
34+ property bool showOnClick: true
35+ property color panelColor: theme.palette.normal.background
36+
37+ property alias alignment: bar.alignment
38+ property alias rowTitle: bar.rowTitle
39+ property alias showRowTitle: bar.showRowTitle
40+ property alias rowItemDelegate: bar.rowItemDelegate
41+ property alias pageDelegate: content.pageDelegate
42+
3443 readonly property real unitProgress: Math.max(0, (height - minimizedPanelHeight) / (openedHeight - minimizedPanelHeight))
3544 readonly property bool fullyOpened: unitProgress >= 1
3645 readonly property bool partiallyOpened: unitProgress > 0 && unitProgress < 1.0
3746 readonly property bool fullyClosed: unitProgress == 0
38− property bool enableHint: true
39− property bool showOnClick: true
40− property color panelColor: theme.palette.normal.background
47+ readonly property alias expanded: bar.expanded
48+ readonly property int barWidth: Math.min(bar.width, bar.implicitWidth)
4149
4250 signal showTapped()
4351
@@ -67,21 +75,20 @@ Showable {
6775 ScriptAction { script: root.height = Qt.binding( function(){ return root.minimizedPanelHeight; } ) }
6876 }
6977
78+ shown: false
7079 height: minimizedPanelHeight
71−
72− onUnitProgressChanged: d.updateState()
7380 clip: root.partiallyOpened
7481
75− IndicatorsLight {
76− id: indicatorLights
77− }
82+ onUnitProgressChanged: d.updateState()
7883
7984 // eater
8085 MouseArea {
81− anchors.fill: parent
86+ anchors.fill: content
8287 hoverEnabled: true
8388 acceptedButtons: Qt.AllButtons
8489 onWheel: wheel.accepted = true;
90+ enabled: root.state != "initial"
91+ visible: content.visible
8592 }
8693
8794 MenuContent {
@@ -94,7 +101,7 @@ Showable {
94101 top: bar.bottom
95102 }
96103 height: openedHeight - bar.height - handle.height
97− indicatorsModel: root.indicatorsModel
104+ model: root.model
98105 visible: root.unitProgress > 0
99106 currentMenuIndex: bar.currentItemIndex
100107 }
@@ -109,6 +116,7 @@ Showable {
109116 }
110117 height: units.gu(2)
111118 active: d.activeDragHandle ? true : false
119+ visible: !root.fullyClosed
112120
113121 //small shadow gradient at bottom of menu
114122 Rectangle {
@@ -129,9 +137,10 @@ Showable {
129137 Rectangle {
130138 anchors.fill: bar
131139 color: panelColor
140+ visible: !root.fullyClosed
132141 }
133142
134− IndicatorsBar {
143+ PanelBar {
135144 id: bar
136145 objectName: "indicatorsBar"
137146
@@ -178,12 +187,14 @@ Showable {
178187
179188 MouseArea {
180189 anchors.bottom: parent.bottom
181− anchors.left: parent.left
182− anchors.right: parent.right
190+ anchors.left: alignment == Qt.AlignLeft ? parent.left : undefined
191+ anchors.right: alignment == Qt.AlignRight ? parent.right : undefined
192+ width: root.barWidth // show handle should only cover panel items.
183193 height: minimizedPanelHeight
184194 enabled: __showDragHandle.enabled && showOnClick
185195 onClicked: {
186− bar.selectItemAt(mouseX)
196+ var barPosition = mapToItem(bar, mouseX, mouseY);
197+ bar.selectItemAt(barPosition.x)
187198 root.show()
188199 }
189200 }
@@ -192,8 +203,9 @@ Showable {
192203 id: __showDragHandle
193204 objectName: "showDragHandle"
194205 anchors.bottom: parent.bottom
195− anchors.left: parent.left
196− anchors.right: parent.right
206+ anchors.left: alignment == Qt.AlignLeft ? parent.left : undefined
207+ anchors.right: alignment == Qt.AlignRight ? parent.right : undefined
208+ width: root.barWidth // show handle should only cover panel items.
197209 height: minimizedPanelHeight
198210 direction: Direction.Downwards
199211 enabled: !root.shown && root.available
@@ -311,7 +323,7 @@ Showable {
311323 script: {
312324 yVelocityCalculator.reset();
313325 // initial item selection
314− if (!d.hasCommitted) bar.selectItemAt(d.activeDragHandle ? d.activeDragHandle.touchPosition.x : -1);
326+ if (!d.hasCommitted) bar.selectItemAt(d.rowMappedLateralPosition);
315327 d.hasCommitted = false;
316328 }
317329 }
qml/Panel/PanelMenuPage.qmlnew file+225
@@ -0,0 +1,225 @@
1+/*
2+ * Copyright 2013-2016 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+import Ubuntu.Components.ListItems 1.3 as ListItems
20+import Unity.Indicators 0.1 as Indicators
21+import "../Components"
22+import "Indicators"
23+
24+PageStack {
25+ id: root
26+
27+ property var submenuIndex: undefined
28+ property QtObject menuModel: null
29+ property Component factory
30+
31+ Connections {
32+ id: dynamicChanges
33+ target: root.menuModel
34+ property bool ready: false
35+
36+ // fix async creation with signal from model before it's finished.
37+ onRowsInserted: {
38+ if (submenuIndex !== undefined && first <= submenuIndex) {
39+ reset(true);
40+ }
41+ }
42+ onRowsRemoved: {
43+ if (submenuIndex !== undefined && first <= submenuIndex) {
44+ reset(true);
45+ }
46+ }
47+ onModelReset: {
48+ if (root.submenuIndex !== undefined) {
49+ reset(true);
50+ }
51+ }
52+ }
53+
54+ Component.onCompleted: {
55+ reset(true);
56+ dynamicChanges.ready = true;
57+ }
58+
59+ function reset(clearModel) {
60+ if (clearModel) {
61+ clear();
62+ var model = submenuIndex == undefined ? menuModel : menuModel.submenu(submenuIndex)
63+ if (model) {
64+ push(pageComponent, { "menuModel": model });
65+ }
66+ } else {
67+ root.currentPage.reset();
68+ }
69+ }
70+
71+ Component {
72+ id: pageComponent
73+ Page {
74+ id: page
75+
76+ property alias menuModel: listView.model
77+ property alias title: backLabel.title
78+ property bool isSubmenu: false
79+
80+ function reset() {
81+ listView.positionViewAtBeginning();
82+ }
83+
84+ property QtObject factory: root.factory.createObject(page, { menuModel: page.menuModel } )
85+
86+ header: PageHeader {
87+ id: backLabel
88+ visible: page.isSubmenu
89+ leadingActionBar.actions: [
90+ Action {
91+ iconName: "back"
92+ text: "Back"
93+ onTriggered: {
94+ root.pop();
95+ }
96+ }
97+ ]
98+ }
99+
100+ ListView {
101+ id: listView
102+ objectName: "listView"
103+
104+ anchors {
105+ top: page.isSubmenu ? backLabel.bottom : parent.top
106+ left: parent.left
107+ right: parent.right
108+ bottom: parent.bottom
109+ bottomMargin: Qt.inputMethod.visible ? (Qt.inputMethod.keyboardRectangle.height - root.anchors.bottomMargin) : 0
110+
111+ Behavior on bottomMargin {
112+ NumberAnimation {
113+ duration: 175
114+ easing.type: Easing.OutQuad
115+ }
116+ }
117+ // TODO - does ever frame.
118+ onBottomMarginChanged: {
119+ listView.positionViewAtIndex(listView.currentIndex, ListView.End)
120+ }
121+ }
122+
123+ // Don't load all the delegates (only max of 3 pages worth -1/0/+1)
124+ cacheBuffer: Math.max(height * 3, units.gu(70))
125+
126+ // Only allow flicking if the content doesn't fit on the page
127+ interactive: contentHeight > height
128+
129+ property int selectedIndex: -1
130+ property bool blockCurrentIndexChange: false
131+ // for count = 0
132+ onCountChanged: {
133+ if (count == 0 && selectedIndex != -1) {
134+ selectedIndex = -1;
135+ }
136+ }
137+ // for highlight following
138+ onSelectedIndexChanged: {
139+ if (currentIndex != selectedIndex) {
140+ var blocked = blockCurrentIndexChange;
141+ blockCurrentIndexChange = true;
142+
143+ currentIndex = selectedIndex;
144+
145+ blockCurrentIndexChange = blocked;
146+ }
147+ }
148+ // for item addition/removal
149+ onCurrentIndexChanged: {
150+ if (!blockCurrentIndexChange) {
151+ if (selectedIndex != -1 && selectedIndex != currentIndex) {
152+ selectedIndex = currentIndex;
153+ }
154+ }
155+ }
156+
157+ Connections {
158+ target: listView.model ? listView.model : null
159+ onRowsAboutToBeRemoved: {
160+ // track current item deletion.
161+ if (listView.selectedIndex >= first && listView.selectedIndex <= last) {
162+ listView.selectedIndex = -1;
163+ }
164+ }
165+ }
166+
167+ delegate: Loader {
168+ id: loader
169+ objectName: "menuItem" + index
170+ width: ListView.view.width
171+ visible: status == Loader.Ready
172+
173+ property int modelIndex: index
174+ sourceComponent: page.factory.load(model)
175+
176+ onLoaded: {
177+ if (item.hasOwnProperty("selected")) {
178+ item.selected = listView.selectedIndex == index;
179+ }
180+ if (item.hasOwnProperty("menuSelected")) {
181+ item.menuSelected.connect(function() { listView.selectedIndex = index; });
182+ }
183+ if (item.hasOwnProperty("menuDeselected")) {
184+ item.menuDeselected.connect(function() { listView.selectedIndex = -1; });
185+ }
186+ if (item.hasOwnProperty("menuData")) {
187+ item.menuData = Qt.binding(function() { return model; });
188+ }
189+ if (item.hasOwnProperty("menuIndex")) {
190+ item.menuIndex = Qt.binding(function() { return modelIndex; });
191+ }
192+ if (item.hasOwnProperty("clicked")) {
193+ item.clicked.connect(function() {
194+ if (model.hasSubmenu) {
195+ root.push(pageComponent, {
196+ "isSubmenu": true,
197+ "title": model.label.replace(/_|&/, ""),
198+ "menuModel": page.menuModel.submenu(modelIndex)
199+ });
200+ }
201+ });
202+ }
203+ }
204+
205+ Binding {
206+ target: item ? item : null
207+ property: "objectName"
208+ value: model.action
209+ }
210+
211+ // TODO: Fixes lp#1243146
212+ // This is a workaround for a Qt bug. https://bugreports.qt-project.org/browse/QTBUG-34351
213+ Connections {
214+ target: listView
215+ onSelectedIndexChanged: {
216+ if (loader.item && loader.item.hasOwnProperty("selected")) {
217+ loader.item.selected = listView.selectedIndex == index;
218+ }
219+ }
220+ }
221+ }
222+ }
223+ }
224+ }
225+}
qml/Rotation/RotationStates.qml+2−2
@@ -273,13 +273,13 @@ StateGroup {
273273 } }
274274 NumberAnimation {
275275 duration: UbuntuAnimation.FastDuration; easing: UbuntuAnimation.StandardEasing
276− target: root.shell; property: "indicatorAreaShowProgress"
276+ target: root.shell; property: "panelAreaShowProgress"
277277 from: 1.0; to: 0.0
278278 }
279279 ImmediateRotationAction { info: d; shell: root.shell }
280280 NumberAnimation {
281281 duration: UbuntuAnimation.FastDuration; easing: UbuntuAnimation.StandardEasing
282− target: root.shell; property: "indicatorAreaShowProgress"
282+ target: root.shell; property: "panelAreaShowProgress"
283283 from: 0.0; to: 1.0
284284 }
285285 ScriptAction { script: {
qml/Shell.qml+20−10
@@ -57,7 +57,7 @@ StyledItem {
5757 property Orientations orientations
5858 property real nativeWidth
5959 property real nativeHeight
60− property alias indicatorAreaShowProgress: panel.indicatorAreaShowProgress
60+ property alias panelAreaShowProgress: panel.panelAreaShowProgress
6161 property string usageScenario: "phone" // supported values: "phone", "tablet" or "desktop"
6262 property string mode: "full-greeter"
6363 property alias oskEnabled: inputMethod.enabled
@@ -313,6 +313,11 @@ StyledItem {
313313 altTabPressed: physicalKeysMapper.altTabPressed
314314 oskEnabled: shell.oskEnabled
315315 spreadEnabled: tutorial.spreadEnabled && (!greeter || (!greeter.hasLockedApp && !greeter.shown))
316+
317+ onSpreadShownChanged: {
318+ panel.indicators.hide();
319+ panel.applicationMenus.hide();
320+ }
316321 }
317322 }
318323
@@ -345,7 +350,7 @@ StyledItem {
345350 Greeter {
346351
347352 enabled: panel.indicators.fullyClosed // hides OSK when panel is open
348− hides: [launcher, panel.indicators]
353+ hides: [launcher, panel.indicators, panel.applicationMenus]
349354 tabletMode: shell.usageScenario != "phone"
350355 forcedUnlock: wizard.active || shell.mode === "full-shell"
351356 background: wallpaperResolver.cachedBackground
@@ -445,6 +450,13 @@ StyledItem {
445450 id: panel
446451 objectName: "panel"
447452 anchors.fill: parent //because this draws indicator menus
453+
454+ mode: shell.usageScenario == "desktop" ? "windowed" : "staged"
455+ minimizedPanelHeight: units.gu(3)
456+ expandedPanelHeight: units.gu(7)
457+ indicatorMenuWidth: parent.width > units.gu(60) ? units.gu(40) : parent.width
458+ applicationMenuWidth: parent.width > units.gu(60) ? units.gu(40) : parent.width
459+
448460 indicators {
449461 hides: [launcher]
450462 available: tutorial.panelEnabled
@@ -452,12 +464,8 @@ StyledItem {
452464 && (!greeter || !greeter.hasLockedApp)
453465 && !shell.waitingOnGreeter
454466 && settings.enableIndicatorMenu
455− width: parent.width > units.gu(60) ? units.gu(40) : parent.width
456467
457− minimizedPanelHeight: units.gu(3)
458− expandedPanelHeight: units.gu(7)
459−
460− indicatorsModel: Indicators.IndicatorsModel {
468+ model: Indicators.IndicatorsModel {
461469 // tablet and phone both use the same profile
462470 // FIXME: use just "phone" for greeter too, but first fix
463471 // greeter app launching to either load the app inside the
@@ -469,8 +477,10 @@ StyledItem {
469477 }
470478 }
471479
472− callHint {
473− greeterShown: greeter.shown
480+ applicationMenus {
481+ hides: [launcher]
482+ available: (!greeter || !greeter.shown)
483+ && !shell.waitingOnGreeter
474484 }
475485
476486 readonly property bool focusedSurfaceIsFullscreen: topLevelSurfaceList.focusedWindow
@@ -478,7 +488,7 @@ StyledItem {
478488 : false
479489 fullscreenMode: (focusedSurfaceIsFullscreen && !LightDMService.greeter.active && launcher.progress == 0)
480490 || greeter.hasLockedApp
481− locked: greeter && greeter.active
491+ greeterShown: greeter && greeter.shown
482492 }
483493
484494 Launcher {
qml/Stage/DecoratedWindow.qml+77−35
@@ -18,6 +18,9 @@ import QtQuick 2.4
1818import Ubuntu.Components 1.3
1919import Unity.Application 0.1
2020import "Spread/MathUtils.js" as MathUtils
21+import Unity.ApplicationMenu 0.1
22+import Unity.Indicators 0.1 as Indicators
23+import "../Components/PanelState"
2124
2225FocusScope {
2326 id: root
@@ -147,46 +150,12 @@ FocusScope {
147150 opacity: root.shadowOpacity
148151 }
149152
150− WindowDecoration {
151− id: decoration
152− closeButtonVisible: root.application.appId !== "unity8-dash"
153− objectName: "appWindowDecoration"
154− anchors { left: parent.left; top: parent.top; right: parent.right }
155− height: units.gu(3)
156− width: root.width
157− title: applicationWindow.title
158− opacity: root.hasDecoration ? Math.min(1, root.showDecoration) : 0
159−
160− Behavior on opacity { UbuntuNumberAnimation { } }
161−
162− onCloseClicked: root.closeClicked();
163− onMaximizeClicked: { root.decorationPressed(); root.maximizeClicked(); }
164− onMaximizeHorizontallyClicked: { root.decorationPressed(); root.maximizeHorizontallyClicked(); }
165− onMaximizeVerticallyClicked: { root.decorationPressed(); root.maximizeVerticallyClicked(); }
166− onMinimizeClicked: root.minimizeClicked();
167− onPressed: root.decorationPressed();
168−
169− onPressedChanged: moveHandler.handlePressedChanged(pressed, pressedButtons, mouseX, mouseY)
170− onPositionChanged: moveHandler.handlePositionChanged(mouse)
171− onReleased: {
172− root.decorationReleased();
173− moveHandler.handleReleased();
174− }
175− }
176−
177− MoveHandler {
178− id: moveHandler
179− objectName: "moveHandler"
180− target: root.parent
181− buttonsWidth: decoration.buttonsWidth
182− }
183−
184153 ApplicationWindow {
185154 id: applicationWindow
186155 objectName: "appWindow"
187− anchors.left: parent.left
188156 anchors.top: parent.top
189157 anchors.topMargin: root.decorationHeight * Math.min(1, root.showDecoration)
158+ anchors.left: parent.left
190159 width: implicitWidth
191160 height: implicitHeight
192161 requestedHeight: !counterRotate ? root.requestedHeight - d.requestedDecorationHeight : root.requestedWidth
@@ -227,6 +196,72 @@ FocusScope {
227196 ]
228197 }
229198
199+ MouseArea {
200+ anchors { left: parent.left; top: parent.top; right: parent.right }
201+ height: units.gu(3)
202+
203+ opacity: root.hasDecoration ? Math.min(1, root.showDecoration) : 0
204+
205+ Behavior on opacity { UbuntuNumberAnimation { } }
206+
207+ drag.target: Item {}
208+ drag.filterChildren: true
209+ drag.threshold: 0
210+
211+ onPressed: root.decorationPressed();
212+ onPressedChanged: moveHandler.handlePressedChanged(pressed, pressedButtons, mouseX, mouseY)
213+ onPositionChanged: moveHandler.handlePositionChanged(mouse)
214+ onReleased: {
215+ root.decorationReleased();
216+ moveHandler.handleReleased();
217+ }
218+
219+ WindowDecoration {
220+ id: decoration
221+ closeButtonVisible: root.application.appId !== "unity8-dash"
222+ objectName: "appWindowDecoration"
223+ anchors.fill: parent
224+ title: applicationWindow.title
225+
226+ onCloseClicked: root.closeClicked();
227+ onMaximizeClicked: { root.decorationPressed(); root.maximizeClicked(); }
228+ onMaximizeHorizontallyClicked: { root.decorationPressed(); root.maximizeHorizontallyClicked(); }
229+ onMaximizeVerticallyClicked: { root.decorationPressed(); root.maximizeVerticallyClicked(); }
230+ onMinimizeClicked: root.minimizeClicked();
231+
232+ enableMenus: {
233+ return active &&
234+ surface &&
235+ (PanelState.focusedPersistentSurfaceId === surface.persistentId && !PanelState.decorationsVisible)
236+ }
237+ menu: sharedAppModel.model
238+
239+ Indicators.SharedUnityMenuModel {
240+ id: sharedAppModel
241+ property var menus: surface ? ApplicationMenuRegistry.getMenusForSurface(surface.persistentId) : []
242+ property var menuService: menus.length > 0 ? menus[0] : undefined
243+
244+ busName: menuService ? menuService.service : ""
245+ menuObjectPath: menuService && menuService.menuPath ? menuService.menuPath : ""
246+ actions: menuService && menuService.actionPath ? { "unity": menuService.actionPath } : {}
247+ }
248+
249+ Connections {
250+ target: ApplicationMenuRegistry
251+ onSurfaceMenuRegistered: {
252+ if (surface && surfaceId === surface.persistentId) {
253+ sharedAppModel.menus = Qt.binding(function() { return surface ? ApplicationMenuRegistry.getMenusForSurface(surface.persistentId) : [] });
254+ }
255+ }
256+ onSurfaceMenuUnregistered: {
257+ if (surface && surfaceId === surface.persistentId) {
258+ sharedAppModel.menus = Qt.binding(function() { return surface ? ApplicationMenuRegistry.getMenusForSurface(surface.persistentId) : [] });
259+ }
260+ }
261+ }
262+ }
263+ }
264+
230265 MouseArea {
231266 anchors.fill: applicationWindow
232267 acceptedButtons: Qt.LeftButton
@@ -257,6 +292,13 @@ FocusScope {
257292 }
258293 }
259294
295+ MoveHandler {
296+ id: moveHandler
297+ objectName: "moveHandler"
298+ target: root.parent
299+ buttonsWidth: decoration.buttonsWidth
300+ }
301+
260302 Rectangle {
261303 anchors.fill: parent
262304 color: "black"
qml/Stage/Stage.qml+17−3
@@ -356,7 +356,7 @@ FocusScope {
356356
357357 Binding {
358358 target: PanelState
359− property: "buttonsVisible"
359+ property: "decorationsVisible"
360360 value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.maximized // FIXME for Locally integrated menus
361361 }
362362
@@ -375,6 +375,20 @@ FocusScope {
375375 when: priv.focusedAppDelegate
376376 }
377377
378+ Binding {
379+ target: PanelState
380+ property: "focusedPersistentSurfaceId"
381+ value: {
382+ if (priv.focusedAppDelegate !== null) {
383+ if (priv.focusedAppDelegate.surface) {
384+ return priv.focusedAppDelegate.surface.persistentId;
385+ }
386+ }
387+ return "";
388+ }
389+ when: priv.focusedAppDelegate
390+ }
391+
378392 Binding {
379393 target: PanelState
380394 property: "dropShadow"
@@ -389,7 +403,7 @@ FocusScope {
389403
390404 Component.onDestruction: {
391405 PanelState.title = "";
392− PanelState.buttonsVisible = false;
406+ PanelState.decorationsVisible = false;
393407 PanelState.dropShadow = false;
394408 }
395409
@@ -1492,7 +1506,7 @@ FocusScope {
14921506
14931507 Binding {
14941508 target: PanelState
1495− property: "buttonsAlwaysVisible"
1509+ property: "decorationsAlwaysVisible"
14961510 value: appDelegate && appDelegate.maximized && touchControls.overlayShown
14971511 }
14981512
qml/Stage/WindowDecoration.qml+80−19
@@ -15,18 +15,23 @@
1515 */
1616
1717import QtQuick 2.4
18+import QtQuick.Layouts 1.1
1819import Ubuntu.Components 1.3
20+import Unity.Application 0.1
1921import "../Components"
22+import "../Components/PanelState"
23+import "../ApplicationMenus"
2024
2125MouseArea {
2226 id: root
23− clip: true
2427
2528 property alias closeButtonVisible: buttons.closeButtonShown
2629 property alias title: titleLabel.text
2730 property alias maximizeButtonShown: buttons.maximizeButtonShown
2831 property bool active: false
2932 property alias overlayShown: buttons.overlayShown
33+ property var menu: undefined
34+ property bool enableMenus: true
3035
3136 readonly property real buttonsWidth: buttons.width + row.spacing
3237
@@ -39,6 +44,7 @@ MouseArea {
3944 signal maximizeHorizontallyClicked()
4045 signal maximizeVerticallyClicked()
4146
47+ onClicked: mouse.accepted = true // propogated event
4248 onDoubleClicked: {
4349 if (mouse.button == Qt.LeftButton) {
4450 root.maximizeClicked();
@@ -48,21 +54,46 @@ MouseArea {
4854 // do not let unhandled wheel event pass thru the decoration
4955 onWheel: wheel.accepted = true;
5056
57+ QtObject {
58+ id: priv
59+ property var menuBar: menuBarLoader.item
60+
61+ property bool shouldShowMenus: root.enableMenus &&
62+ menuBar &&
63+ menuBar.valid &&
64+ (menuBar.showRequested || root.containsMouse)
65+ }
66+
67+ // We dont want touch events to fall through to parent,
68+ // otherwise the containsMouse will not work.
69+ MouseArea {
70+ anchors.fill: parent
71+ propagateComposedEvents: true
72+ }
73+
5174 Rectangle {
75+ id: background
5276 anchors.fill: parent
53− anchors.bottomMargin: -radius
5477 radius: units.gu(.5)
5578 color: theme.palette.normal.background
5679 }
5780
58− Row {
81+ Rectangle {
82+ anchors {
83+ bottom: background.bottom
84+ left: parent.left
85+ right: parent.right
86+ }
87+ height: background.radius
88+ color: theme.palette.normal.background
89+ }
90+
91+ RowLayout {
5992 id: row
6093 anchors {
6194 fill: parent
6295 leftMargin: overlayShown ? units.gu(5) : units.gu(1)
6396 rightMargin: units.gu(1)
64− topMargin: units.gu(0.5)
65− bottomMargin: units.gu(0.5)
6697 }
6798 Behavior on anchors.leftMargin {
6899 UbuntuNumberAnimation {}
@@ -72,7 +103,12 @@ MouseArea {
72103
73104 WindowControlButtons {
74105 id: buttons
75− height: parent.height
106+ anchors {
107+ top: parent.top
108+ bottom: parent.bottom
109+ topMargin: units.gu(0.5)
110+ bottomMargin: units.gu(0.5)
111+ }
76112 active: root.active
77113 onCloseClicked: root.closeClicked();
78114 onMinimizeClicked: root.minimizeClicked();
@@ -81,19 +117,44 @@ MouseArea {
81117 onMaximizeVerticallyClicked: root.maximizeVerticallyClicked();
82118 }
83119
84− Label {
85− id: titleLabel
86− objectName: "windowDecorationTitle"
87− color: root.active ? "white" : UbuntuColors.slate
88− height: parent.height
89− width: parent.width - buttons.width - parent.anchors.rightMargin - parent.anchors.leftMargin
90− verticalAlignment: Text.AlignVCenter
91− fontSize: "medium"
92− font.weight: root.active ? Font.Light : Font.Medium
93− elide: Text.ElideRight
94− opacity: overlayShown ? 0 : 1
95− visible: opacity != 0
96− Behavior on opacity { UbuntuNumberAnimation {} }
120+ Item {
121+ Layout.preferredHeight: parent.height
122+ Layout.fillWidth: true
123+
124+ Label {
125+ id: titleLabel
126+ objectName: "windowDecorationTitle"
127+ color: root.active ? "white" : UbuntuColors.slate
128+ height: parent.height
129+ width: parent.width
130+ verticalAlignment: Text.AlignVCenter
131+ fontSize: "medium"
132+ font.weight: root.active ? Font.Light : Font.Medium
133+ elide: Text.ElideRight
134+ opacity: overlayShown || priv.shouldShowMenus ? 0 : 1
135+ visible: opacity != 0
136+ Behavior on opacity { UbuntuNumberAnimation {} }
137+ }
138+
139+ Loader {
140+ id: menuBarLoader
141+ objectName: "menuBarLoader"
142+ anchors.bottom: parent.bottom
143+ height: parent.height
144+ width: parent.width
145+ active: root.menu !== undefined
146+
147+ sourceComponent: MenuBar {
148+ id: menuBar
149+ height: menuBarLoader.height
150+ enableKeyFilter: valid && root.active && root.enableMenus
151+ unityMenuModel: root.menu
152+ }
153+
154+ opacity: !overlayShown && priv.shouldShowMenus ? 1 : 0
155+ visible: opacity == 1
156+ Behavior on opacity { UbuntuNumberAnimation {} }
157+ }
97158 }
98159 }
99160}
src/main.cpp+2−2
@@ -27,8 +27,8 @@ int main(int argc, const char *argv[])
2727{
2828 qSetMessagePattern("[%{time yyyy-MM-dd:hh:mm:ss.zzz}] %{if-category}%{category}: %{endif}%{message}");
2929
30− bool isMirServer = false;
31− if (qgetenv("QT_QPA_PLATFORM") == "ubuntumirclient") {
30+ bool isMirServer = qgetenv("QT_QPA_PLATFORM") == "mirserver";
31+ if (!isMirServer && qgetenv("QT_QPA_PLATFORM") == "ubuntumirclient") {
3232 setenv("QT_QPA_PLATFORM", "mirserver", 1 /* overwrite */);
3333 isMirServer = true;
3434 }
tests/mocks/QMenuModel/unitymenumodel.cpp+47−6
@@ -29,7 +29,9 @@ enum MenuRoles {
2929 ActionStateRole,
3030 IsCheckRole,
3131 IsRadioRole,
32− IsToggledRole
32+ IsToggledRole,
33+ ShortcutRole,
34+ HasSubmenuRole
3335};
3436
3537UnityMenuModel::UnityMenuModel(QObject *parent)
@@ -141,7 +143,29 @@ int UnityMenuModel::columnCount(const QModelIndex&) const
141143
142144QVariant UnityMenuModel::data(const QModelIndex &index, int role) const
143145{
144− return rowData(index.row())[roleNames()[role]];
146+ QVariantMap v = rowData(index.row());
147+ QString roleName = roleNames()[role];
148+
149+ if (v.contains(roleName)) return v[roleName];
150+
151+ // defaults
152+ switch (role) {
153+ case LabelRole: return QString();
154+ case SensitiveRole: return true;
155+ case IsSeparatorRole: return false;
156+ case IconRole: return QString();
157+ case TypeRole: return QString();
158+ case ExtendedAttributesRole: return QVariantMap();
159+ case ActionRole: return QString();
160+ case ActionStateRole: return QVariant();
161+ case IsCheckRole: return false;
162+ case IsRadioRole: return false;
163+ case IsToggledRole: return false;
164+ case ShortcutRole: return QString();
165+ case HasSubmenuRole: return subMenuData(index.row()).isValid();
166+ default: break;
167+ }
168+ return QVariant();
145169}
146170
147171QVariantMap UnityMenuModel::rowData(int row) const
@@ -149,12 +173,14 @@ QVariantMap UnityMenuModel::rowData(int row) const
149173 if (m_modelData.count() <= row) {
150174 return QVariantMap();
151175 }
152− return m_modelData[row].toMap()["rowData"].toMap();
176+ QVariantMap vRow = m_modelData.value(row, QVariantMap()).toMap();
177+ return vRow["rowData"].toMap();
153178}
154179
155180QVariant UnityMenuModel::subMenuData(int row) const
156181{
157− return m_modelData[row].toMap()["submenu"];
182+ QVariantMap v = m_modelData.value(row, QVariantMap()).toMap();
183+ return v.value("submenu", QVariant());
158184}
159185
160186QModelIndex UnityMenuModel::index(int row, int column, const QModelIndex&) const
@@ -182,6 +208,8 @@ QHash<int, QByteArray> UnityMenuModel::roleNames() const
182208 names[IsCheckRole] = "isCheck";
183209 names[IsRadioRole] = "isRadio";
184210 names[IsToggledRole] = "isToggled";
211+ names[ShortcutRole] = "shortcut";
212+ names[HasSubmenuRole] = "hasSubmenu";
185213
186214 return names;
187215}
@@ -201,6 +229,7 @@ QObject * UnityMenuModel::submenu(int position, QQmlComponent*)
201229 UnityMenuModel*& model = submenus[position];
202230 if (!model) {
203231 model = new UnityMenuModel(this);
232+ connect(model, &UnityMenuModel::activated, this, &UnityMenuModel::activated);
204233 }
205234 if (model->modelData() != submenuData) {
206235 model->setModelData(submenuData);
@@ -225,11 +254,23 @@ QVariant UnityMenuModel::get(int row, const QByteArray &role)
225254 roles.insert(names[role], role);
226255 }
227256
228− return this->data(this->index(row, 0), roles[role]);
257+ return data(index(row, 0), roles[role]);
229258}
230259
231−void UnityMenuModel::activate(int, const QVariant&)
260+void UnityMenuModel::activate(int row, const QVariant&)
232261{
262+ QVariantMap vModelData = m_modelData.value(row, QVariantMap()).toMap();
263+ QVariantMap rd = vModelData["rowData"].toMap();
264+
265+ bool isCheckable = rd[roleNames()[IsCheckRole]].toBool() || rd[roleNames()[IsRadioRole]].toBool();
266+ if (isCheckable) {
267+ rd[roleNames()[IsToggledRole]] = !rd[roleNames()[IsToggledRole]].toBool();
268+ vModelData["rowData"] = rd;
269+ m_modelData[row] = vModelData;
270+
271+ dataChanged(index(row, 0), index(row, 0), QVector<int>() << IsToggledRole);
272+ }
273+ Q_EMIT activated(rd[roleNames()[ActionRole]].toString());
233274}
234275
235276void UnityMenuModel::changeState(int, const QVariant&)
tests/mocks/QMenuModel/unitymenumodel.h+3−1
@@ -61,7 +61,7 @@ public:
6161
6262 QString nameOwner() const;
6363
64− int rowCount(const QModelIndex &parent = QModelIndex()) const override;
64+ Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const override;
6565 int columnCount(const QModelIndex &parent = QModelIndex()) const override;
6666 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
6767 QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const override;
@@ -88,6 +88,8 @@ Q_SIGNALS:
8888 // Internal mock usage
8989 void modelDataChanged();
9090
91+ void activated(const QString& action);
92+
9193private:
9294 QVariantMap rowData(int row) const;
9395 QVariant subMenuData(int row) const;
tests/mocks/Unity/Application/SurfaceManager.cpp+5−2
@@ -83,9 +83,11 @@ void SurfaceManager::registerSurface(MirSurface *surface)
8383 this->onStateRequested(surface, state);
8484 });
8585
86+ const QString persistentId = surface->persistentId();
8687 connect(surface, &QObject::destroyed, this, [=]() {
87− this->onSurfaceDestroyed(surface);
88+ this->onSurfaceDestroyed(surface, persistentId);
8889 });
90+
8991}
9092
9193void SurfaceManager::notifySurfaceCreated(unityapi::MirSurfaceInterface *surface)
@@ -223,7 +225,7 @@ void SurfaceManager::onStateRequested(MirSurface *surface, Mir::State state)
223225 DEBUG_MSG("("<<surface<<","<<state<<") ended");
224226}
225227
226−void SurfaceManager::onSurfaceDestroyed(MirSurface *surface)
228+void SurfaceManager::onSurfaceDestroyed(MirSurface *surface, const QString& persistentId)
227229{
228230 m_surfaces.removeAll(surface);
229231 if (m_focusedSurface == surface) {
@@ -237,6 +239,7 @@ void SurfaceManager::onSurfaceDestroyed(MirSurface *surface)
237239 m_underModification = false;
238240 Q_EMIT modificationsEnded();
239241 }
242+ Q_EMIT surfaceDestroyed(persistentId);
240243}
241244
242245void SurfaceManager::focusFirstAvailableSurface()
tests/mocks/Unity/Application/SurfaceManager.h+3−1
@@ -76,6 +76,8 @@ public Q_SLOTS:
7676 void createInputMethodSurface();
7777
7878Q_SIGNALS:
79+ void surfaceDestroyed(const QString& persistentSurfaceId);
80+
7981 void newSurfaceMinimumWidthChanged(int value);
8082 void newSurfaceMaximumWidthChanged(int value);
8183 void newSurfaceMinimumHeightChanged(int value);
@@ -85,7 +87,7 @@ Q_SIGNALS:
8587
8688private Q_SLOTS:
8789 void onStateRequested(MirSurface *surface, Mir::State state);
88− void onSurfaceDestroyed(MirSurface *surface);
90+ void onSurfaceDestroyed(MirSurface *surface, const QString& persistentId);
8991
9092private:
9193 void doRaise(unity::shell::application::MirSurfaceInterface *surface);
tests/mocks/Unity/ApplicationMenu/mockapplicationmenuregistry.cpp+5
@@ -40,3 +40,8 @@ void MockApplicationMenuRegistry::RegisterSurfaceMenu(const QString &surface,
4040 QDBusObjectPath(actionObjectPath),
4141 service);
4242}
43+
44+void MockApplicationMenuRegistry::UnregisterSurfaceMenu(const QString &surfaceId, const QDBusObjectPath &menuObjectPath)
45+{
46+ ApplicationMenuRegistry::UnregisterSurfaceMenu(surfaceId, menuObjectPath);
47+}
tests/mocks/Unity/ApplicationMenu/mockapplicationmenuregistry.h+3
@@ -31,6 +31,9 @@ public:
3131 const QString &actionObjectPath,
3232 const QString &service);
3333
34+ Q_INVOKABLE void UnregisterSurfaceMenu(const QString &surfaceId,
35+ const QDBusObjectPath &menuObjectPath);
36+
3437private:
3538 MockApplicationMenuRegistry(QObject *parent = 0);
3639};
tests/mocks/Unity/Indicators/fakeindicatorsmodeldata.js+4−4
@@ -567,7 +567,7 @@ var fakeMenuData = {
567567 "state": "Stopped"
568568 },
569569 "ext": {},
570− "icon": "file:///usr/share/mediaplayer-app/mediaplayer-app.png",
570+ "icon": "image://theme/mediaplayer-app",
571571 "isCheck": false,
572572 "isRadio": false,
573573 "isSeparator": false,
@@ -863,7 +863,7 @@ var fakeMenuData = {
863863 "ext": {
864864 "xCanonicalUid": 1003
865865 },
866− "icon": "file:///usr/share/webbrowser-app/webbrowser-app.png",
866+ "icon": "image://theme/webbrowser-app",
867867 "isCheck": false,
868868 "isRadio": false,
869869 "isSeparator": false,
@@ -912,7 +912,7 @@ var fakeMenuData = {
912912 "ext": {
913913 "xCanonicalUid": 1002
914914 },
915− "icon": "file:///usr/share/webbrowser-app/webbrowser-app.png",
915+ "icon": "image://theme/webbrowser-app",
916916 "isCheck": false,
917917 "isRadio": false,
918918 "isSeparator": false,
@@ -929,7 +929,7 @@ var fakeMenuData = {
929929 "ext": {
930930 "xCanonicalUid": 1001
931931 },
932− "icon": "file:///usr/share/webbrowser-app/webbrowser-app.png",
932+ "icon": "image://theme/webbrowser-app",
933933 "isCheck": false,
934934 "isRadio": false,
935935 "isSeparator": false,
tests/qmltests/ApplicationMenuDataLoader.qmlnew file+75
@@ -0,0 +1,75 @@
1+import QtQuick 2.4
2+import Ubuntu.Components 1.3
3+import Unity.Application 0.1
4+import Unity.ApplicationMenu 0.1
5+import Unity.Indicators 0.1 as Indicators
6+
7+Object {
8+
9+ property alias surfaceManager: sMgrHandler.target
10+
11+ Connections {
12+ id: sMgrHandler
13+ onSurfaceCreated: {
14+ var fakeMenuPath = "/" + surface.persistentId.replace(/\W+/g, "");
15+
16+ ApplicationMenuRegistry.RegisterSurfaceMenu(surface.persistentId, fakeMenuPath, fakeMenuPath, ":1");
17+ Indicators.UnityMenuModelCache.setCachedModelData(fakeMenuPath, generateTestData(4, 3, 2, 3, "menu"));
18+ }
19+ onSurfaceDestroyed: {
20+ ApplicationMenuRegistry.UnregisterSurfaceMenu(persistentSurfaceId, "/app");
21+ }
22+ }
23+
24+ function generateTestData(length, depth, submenuInterval, separatorInterval, prefix, root) {
25+ var data = [];
26+ if (root === undefined) root = true;
27+
28+ if (prefix === undefined) prefix = "menu"
29+
30+ for (var i = 0; i < length; i++) {
31+
32+ var menuCode = String.fromCharCode(i+65);
33+
34+ var isSeparator = !root && separatorInterval > 0 && ((i+1) % separatorInterval == 0);
35+ var row = {
36+ "rowData": { // 1
37+ "label": prefix + "&" + menuCode,
38+ "sensitive": true,
39+ "isSeparator": isSeparator,
40+ "icon": "",
41+ "ext": {},
42+ "action": prefix + menuCode,
43+ "actionState": {},
44+ "isCheck": false,
45+ "isRadio": false,
46+ "isToggled": false,
47+ "shortcut": ""
48+ }
49+ }
50+ var isSubmenu = root === undefined || root === true || (submenuInterval > 0 && ((i+1) % submenuInterval == 0));
51+ if (isSubmenu && !isSeparator && depth > 1) {
52+ row["submenu"] = generateTestData(length, depth-1, submenuInterval, separatorInterval, prefix + menuCode + ".", false);
53+ }
54+ data[i] = row;
55+ }
56+ return data;
57+ }
58+
59+ // Test Data
60+ property var singleCheckable: [{
61+ "rowData": { // 1
62+ "label": "checkable1",
63+ "sensitive": true,
64+ "isSeparator": false,
65+ "icon": "",
66+ "ext": {},
67+ "action": "checkable1",
68+ "actionState": {},
69+ "isCheck": true,
70+ "isRadio": false,
71+ "isToggled": false,
72+ "shortcut": "Alt+F"
73+ }
74+ }]
75+}
tests/qmltests/ApplicationMenus/tst_MenuBar.qmlnew file+183
@@ -0,0 +1,183 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtTest 1.0
19+import Ubuntu.Components 1.3
20+import Ubuntu.Components.ListItems 1.3
21+import Unity.Application 0.1
22+import QMenuModel 0.1
23+import Unity.Test 0.1
24+import Utils 0.1
25+
26+import "../../../qml/ApplicationMenus"
27+import ".."
28+
29+Item {
30+ id: root
31+ width: units.gu(100)
32+ height: units.gu(50)
33+
34+ Component.onCompleted: {
35+ QuickUtils.keyboardAttached = true;
36+ theme.name = "Ubuntu.Components.Themes.SuruDark"
37+ }
38+
39+ Binding {
40+ target: MouseTouchAdaptor
41+ property: "enabled"
42+ value: false
43+ }
44+
45+ SurfaceManager { id: sMgr }
46+ ApplicationMenuDataLoader {
47+ id: appMenuData
48+ surfaceManager: sMgr
49+ }
50+
51+ Rectangle {
52+ anchors {
53+ left: parent.left
54+ right: parent.right
55+ top: parent.top
56+ margins: units.gu(1)
57+ }
58+ height: units.gu(3)
59+ color: "grey"
60+
61+ MenuBar {
62+ id: menuBar
63+ anchors.fill: parent
64+ enableKeyFilter: true
65+
66+ unityMenuModel: UnityMenuModel {
67+ id: menuBackend
68+ modelData: appMenuData.generateTestData(7,5,2,3,"menu")
69+ }
70+ }
71+ }
72+
73+ SignalSpy {
74+ id: activatedSpy
75+ target: menuBackend
76+ signalName: "activated"
77+ }
78+
79+ UnityTestCase {
80+ id: testCase
81+ name: "MenuBar"
82+ when: windowShown
83+
84+ function init() {
85+ menuBar.dismiss();
86+ menuBackend.modelData = appMenuData.generateTestData(5,5,2,3,"menu")
87+ activatedSpy.clear();
88+ }
89+
90+ function test_mouseNavigation() {
91+ menuBackend.modelData = appMenuData.generateTestData(3,3,0,0,"menu");
92+ wait(50) // wait for row to build
93+ var priv = findInvisibleChild(menuBar, "d");
94+
95+ var menuItem0 = findChild(menuBar, "menuBar-item0"); verify(menuItem0);
96+ var menuItem1 = findChild(menuBar, "menuBar-item1"); verify(menuItem1);
97+ var menuItem2 = findChild(menuBar, "menuBar-item2"); verify(menuItem2);
98+
99+ menuItem0.show();
100+ compare(priv.currentItem, menuItem0, "CurrentItem should be set to item 0");
101+ compare(priv.currentItem.popupVisible, true, "Popup should be visible");
102+
103+ mouseMove(menuItem1, menuItem1.width/2, menuItem1.height/2);
104+ tryCompare(priv, "currentItem", menuItem1, undefined, "CurrentItem should have moved to item 1");
105+ compare(menuItem1.popupVisible, true, "Popup should be visible");
106+
107+ mouseMove(menuItem2, menuItem2.width/2, menuItem2.height/2);
108+ tryCompare(priv, "currentItem", menuItem2, undefined, "CurrentItem should have moved to item 2");
109+ compare(menuItem2.popupVisible, true, "Popup should be visible");
110+
111+ mouseMove(menuItem0, menuItem0.width/2, menuItem0.height/2);
112+ tryCompare(priv, "currentItem", menuItem0, undefined, "CurrentItem should have moved to item 0");
113+ compare(menuItem0.popupVisible, true, "Popup should be visible");
114+ }
115+
116+ function test_keyboardNavigation_RightKeySelectsNextMenuItem(data) {
117+ menuBackend.modelData = appMenuData.generateTestData(3,3,0,0,"menu");
118+ var priv = findInvisibleChild(menuBar, "d");
119+
120+ var menuItem0 = findChild(menuBar, "menuBar-item0"); verify(menuItem0);
121+ var menuItem1 = findChild(menuBar, "menuBar-item1"); verify(menuItem1);
122+ var menuItem2 = findChild(menuBar, "menuBar-item2"); verify(menuItem2);
123+
124+ menuItem0.show();
125+ compare(priv.currentItem, menuItem0, "CurrentItem should be set to item 0");
126+ compare(priv.currentItem.popupVisible, true, "Popup should be visible");
127+
128+ keyClick(Qt.Key_Right, Qt.NoModifier);
129+ compare(priv.currentItem, menuItem1, "CurrentItem should have moved to item 1");
130+ compare(menuItem1.popupVisible, true, "Popup should be visible");
131+
132+ keyClick(Qt.Key_Right, Qt.NoModifier);
133+ compare(priv.currentItem, menuItem2, "CurrentItem should have moved to item 2");
134+ compare(menuItem2.popupVisible, true, "Popup should be visible");
135+
136+ keyClick(Qt.Key_Right, Qt.NoModifier);
137+ compare(priv.currentItem, menuItem0, "CurrentItem should have moved back to item 0");
138+ compare(menuItem0.popupVisible, true, "Popup should be visible");
139+ }
140+
141+ function test_keyboardNavigation_LeftKeySelectsPreviousMenuItem(data) {
142+ menuBackend.modelData = appMenuData.generateTestData(3,3,0,0,"menu");
143+ var priv = findInvisibleChild(menuBar, "d");
144+
145+ var menuItem0 = findChild(menuBar, "menuBar-item0"); verify(menuItem0);
146+ var menuItem1 = findChild(menuBar, "menuBar-item1"); verify(menuItem1);
147+ var menuItem2 = findChild(menuBar, "menuBar-item2"); verify(menuItem2);
148+
149+ menuItem0.show();
150+ compare(priv.currentItem, menuItem0, "CurrentItem should be set to item 0");
151+ compare(priv.currentItem.popupVisible, true, "Popup should be visible");
152+
153+ keyClick(Qt.Key_Left, Qt.NoModifier);
154+ compare(priv.currentItem, menuItem2, "CurrentItem should have moved to item 2");
155+ compare(menuItem2.popupVisible, true, "Popup should be visible");
156+
157+ keyClick(Qt.Key_Left, Qt.NoModifier);
158+ compare(priv.currentItem, menuItem1, "CurrentItem should have moved to item 1");
159+ compare(menuItem1.popupVisible, true, "Popup should be visible");
160+
161+ keyClick(Qt.Key_Left, Qt.NoModifier);
162+ compare(priv.currentItem, menuItem0, "CurrentItem should have moved back to item 0");
163+ compare(menuItem0.popupVisible, true, "Popup should be visible");
164+ }
165+
166+ function test_mnemonics_data() {
167+ return [
168+ { tag: "a", expectedItem: "menuBar-item0" },
169+ { tag: "c", expectedItem: "menuBar-item2" },
170+ ]
171+ }
172+
173+ function test_mnemonics(data) {
174+ menuBackend.modelData = appMenuData.generateTestData(3,3,0,0,"menu");
175+ var priv = findInvisibleChild(menuBar, "d");
176+
177+ var menuItem = findChild(menuBar, data.expectedItem); verify(menuItem);
178+
179+ keyPress(data.tag, Qt.AltModifier, 100);
180+ tryCompare(priv, "currentItem", menuItem);
181+ }
182+ }
183+}
tests/qmltests/ApplicationMenus/tst_MenuPopup.qmlnew file+207
@@ -0,0 +1,207 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtTest 1.0
19+import Ubuntu.Components 1.3
20+import Ubuntu.Components.ListItems 1.3
21+import Unity.Application 0.1
22+import QMenuModel 0.1
23+import Unity.Test 0.1
24+import Utils 0.1
25+
26+import "../../../qml/ApplicationMenus"
27+import ".."
28+
29+Item {
30+ id: root
31+ width: Math.max(units.gu(100), page.width + units.gu(6))
32+ height: Math.max(units.gu(50), page.height + units.gu(6))
33+
34+ Binding {
35+ target: MouseTouchAdaptor
36+ property: "enabled"
37+ value: false
38+ }
39+
40+ ApplicationMenuDataLoader { id: appMenuData }
41+
42+ MenuPopup {
43+ id: page
44+
45+ anchors {
46+ left: parent.left
47+ top: parent.top
48+ leftMargin: units.gu(3)
49+ topMargin: units.gu(3)
50+ }
51+
52+ unityMenuModel: UnityMenuModel {
53+ id: menuBackend
54+ modelData: appMenuData.generateTestData(7,5,2,3,"menu")
55+ }
56+ }
57+
58+ SignalSpy {
59+ id: activatedSpy
60+ target: menuBackend
61+ signalName: "activated"
62+ }
63+
64+ UnityTestCase {
65+ id: testCase
66+ name: "MenuPopup"
67+ when: windowShown
68+
69+ function init() {
70+ page.show();
71+ }
72+
73+ function cleanup() {
74+ page.reset();
75+ wait(100); // let the page dismiss
76+ activatedSpy.clear();
77+ }
78+
79+ // visit and verify that all the backend menus have been created
80+ function recurseMenuConstruction(rows, menuPage) {
81+ for (var i = 0; i < rows.length; ++i) {
82+ var rowData = rows[i]["rowData"];
83+
84+ var menuItemName = menuPage.objectName +"-item"+i
85+
86+ var menuItem = findChild(menuPage, menuItemName); verify(menuItem);
87+
88+ var menuPriv = findInvisibleChild(menuPage, "d");
89+
90+ // recurse into submenu
91+ var submenu = rows[i]["submenu"];
92+ if (submenu) {
93+ mouseClick(menuItem, menuItem.width/2, menuItem.height/2);
94+ tryCompare(menuPriv, "currentItem", menuItem);
95+
96+ tryCompareFunction(function() { return menuItem.popup !== null && menuItem.visible }, true);
97+
98+ var submenuPage = findChild(menuPage, menuItemName + "-menu"); verify(submenuPage);
99+
100+ recurseMenuConstruction(submenu, submenuPage);
101+ } else {
102+ mouseMove(menuItem, menuItem.width/2, menuItem.height/2);
103+ tryCompare(menuPriv, "currentItem", menuItem);
104+ }
105+ }
106+ }
107+
108+ function test_mouseNavigation_data() {
109+ return [
110+ { tag: "long", testData: appMenuData.generateTestData(4, 2, 1, 0, "menu", false) },
111+ { tag: "deep", testData: appMenuData.generateTestData(2, 4, 1, 0, "menu", false) }
112+ ]
113+ }
114+
115+ function test_mouseNavigation(data) {
116+ menuBackend.modelData = data.testData;
117+
118+ recurseMenuConstruction(data.testData, page);
119+ }
120+
121+ function test_checkableMenuTogglesOnClick() {
122+ menuBackend.modelData = appMenuData.singleCheckable;
123+
124+ var menuItem = findChild(page, "menu-item0-actionItem");
125+ verify(menuItem);
126+ compare(menuItem.action.checkable, true, "Menu item should be checkable");
127+ compare(menuItem.action.checked, false, "Menu item should not be checked");
128+
129+ mouseClick(menuItem, menuItem.width/2, menuItem.height/2);
130+
131+ compare(menuItem.action.checked, true, "Checkable menu item should have toggled");
132+ }
133+
134+ function test_keyboardNavigation_DownKeySelectsAndOpensNextMenuItemAndRotates() {
135+ menuBackend.modelData = appMenuData.generateTestData(3,3,0,0,"menu",false);
136+
137+ var item0 = findChild(page, "menu-item0"); verify(item0);
138+ var item1 = findChild(page, "menu-item1"); verify(item1);
139+ var item2 = findChild(page, "menu-item2"); verify(item2);
140+
141+ var priv = findInvisibleChild(page, "d");
142+
143+ keyClick(Qt.Key_Down, Qt.NoModifier);
144+ compare(priv.currentItem, item0, "CurrentItem should have moved to item 0");
145+
146+ keyClick(Qt.Key_Down, Qt.NoModifier);
147+ compare(priv.currentItem, item1, "CurrentItem should have moved to item 1");
148+
149+ keyClick(Qt.Key_Down, Qt.NoModifier);
150+ compare(priv.currentItem, item2, "CurrentItem should have moved to item 2");
151+
152+ keyClick(Qt.Key_Down, Qt.NoModifier);
153+ compare(priv.currentItem, item0, "CurrentItem should have moved to item 0");
154+ }
155+
156+ function test_keyboardNavigation_UpKeySelectsAndOpensPreviousMenuItemAndRotates() {
157+ menuBackend.modelData = appMenuData.generateTestData(3,3,0,0,"menu",false);
158+
159+ var item0 = findChild(page, "menu-item0"); verify(item0);
160+ var item1 = findChild(page, "menu-item1"); verify(item1);
161+ var item2 = findChild(page, "menu-item2"); verify(item2);
162+
163+ var priv = findInvisibleChild(page, "d");
164+
165+ keyClick(Qt.Key_Down, Qt.NoModifier);
166+ compare(priv.currentItem, item0, "CurrentItem should have moved to item 2");
167+
168+ keyClick(Qt.Key_Down, Qt.NoModifier);
169+ compare(priv.currentItem, item1, "CurrentItem should have moved to item 1");
170+
171+ keyClick(Qt.Key_Down, Qt.NoModifier);
172+ compare(priv.currentItem, item2, "CurrentItem should have moved to item 0");
173+
174+ keyClick(Qt.Key_Down, Qt.NoModifier);
175+ compare(priv.currentItem, item0, "CurrentItem should have moved to item 2");
176+ }
177+
178+ function test_keyboardNavigation_RightKeyEntersSubMenu() {
179+ menuBackend.modelData = appMenuData.generateTestData(3,3,1,0,"menu",false);
180+
181+ var menuItem = findChild(page, "menu-item0");
182+
183+ var priv = findInvisibleChild(page, "d");
184+ priv.currentItem = menuItem;
185+
186+ keyClick(Qt.Key_Right, Qt.NoModifier);
187+ tryCompareFunction(function() { return menuItem.popup !== null && menuItem.popup.visible }, true);
188+
189+ var submenu0 = findChild(page, "menu-item0-menu"); verify(submenu0);
190+ var submenu0item0 = findChild(submenu0, "menu-item0-menu-item0"); verify(submenu0item0);
191+
192+ var submenu0Priv = findInvisibleChild(submenu0, "d"); verify(submenu0Priv);
193+ compare(submenu0Priv.currentItem, submenu0item0, "First item of submenu should be selected");
194+ }
195+
196+ function test_keyboardNavigation_LeftKeyClosesSubMenu() {
197+ menuBackend.modelData = appMenuData.generateTestData(3,3,1,0,"menu",false);
198+
199+ var menuItem = findChild(page, "menu-item0"); verify(menuItem);
200+ mouseClick(menuItem, menuItem.width/2, menuItem.height/2);
201+ tryCompareFunction(function() { return menuItem.popup !== null && menuItem.popup.visible }, true);
202+
203+ keyClick(Qt.Key_Left, Qt.NoModifier);
204+ tryCompareFunction(function() { return menuItem.popup !== null && menuItem.popup.visible }, false);
205+ }
206+ }
207+}
tests/qmltests/CMakeLists.txt+11−7
@@ -7,6 +7,9 @@ add_unity8_qmltest(. Shell)
77add_unity8_qmltest(. ShellWithPin)
88add_unity8_qmltest(. DeviceConfiguration)
99add_unity8_qmltest_data(. EdgeBarrierControls)
10+add_unity8_qmltest_data(. ApplicationMenuDataLoader)
11+add_unity8_qmltest(ApplicationMenus MenuBar)
12+add_unity8_qmltest(ApplicationMenus MenuPopup)
1013add_unity8_qmltest(Components Background)
1114add_unity8_qmltest(Components Carousel)
1215add_unity8_qmltest(Components Dialogs)
@@ -85,16 +88,16 @@ add_unity8_qmltest(Notifications OptionToggle)
8588add_unity8_qmltest(Notifications SwipeToAct)
8689add_unity8_qmltest_data(Notifications Notification)
8790add_unity8_qmltest(Panel ActiveCallHint)
88−add_unity8_qmltest(Panel IndicatorItem)
89−add_unity8_qmltest(Panel IndicatorItemRow)
90−add_unity8_qmltest(Panel IndicatorPage)
91−add_unity8_qmltest(Panel IndicatorsBar)
92−add_unity8_qmltest(Panel IndicatorsMenu)
91+add_unity8_qmltest(Panel PanelItemRow)
92+add_unity8_qmltest(Panel PanelMenuPage)
93+add_unity8_qmltest(Panel PanelBar)
94+add_unity8_qmltest(Panel PanelMenu)
9395add_unity8_qmltest(Panel MenuContent)
9496add_unity8_qmltest(Panel Panel)
95−add_unity8_qmltest_data(Panel IndicatorTest)
97+add_unity8_qmltest_data(Panel PanelTest)
98+add_unity8_qmltest(Panel/Indicators IndicatorItem)
9699add_unity8_qmltest(Panel/Indicators IndicatorsLight)
97−add_unity8_qmltest(Panel/Indicators MenuItemFactory)
100+add_unity8_qmltest(Panel/Indicators IndicatorMenuItemFactory)
98101add_unity8_qmltest(Panel/Indicators MessageMenuItemFactory)
99102add_unity8_qmltest(Stage ApplicationWindow)
100103add_unity8_qmltest(Stage DesktopStage)
@@ -104,6 +107,7 @@ add_unity8_qmltest(Stage TabletStage)
104107add_unity8_qmltest(Stage WindowResizeArea)
105108add_unity8_qmltest(Stage Splash)
106109add_unity8_qmltest(Stage DecoratedWindow)
110+add_unity8_qmltest(Stage WindowDecoration)
107111add_unity8_qmltest_data(Stage ApplicationCheckBox)
108112add_unity8_qmltest_data(Stage SizeHintField)
109113add_unity8_qmltest_data(Stage SurfaceManagerControls)
tests/qmltests/Panel/tst_IndicatorItem.qml → tests/qmltests/Panel/Indicators/tst_IndicatorItem.qmlrenamed+1−1
@@ -19,7 +19,7 @@ import QtQuick.Layouts 1.1
1919import QtTest 1.0
2020import Ubuntu.Components 1.3
2121import Unity.Test 0.1 as UT
22−import "../../../qml/Panel"
22+import "../../../../qml/Panel/Indicators"
2323
2424Rectangle {
2525 width: units.gu(80)
tests/qmltests/Panel/Indicators/tst_MenuItemFactory.qml → tests/qmltests/Panel/Indicators/tst_IndicatorMenuItemFactory.qmlrenamed+2−2
@@ -28,7 +28,7 @@ Item {
2828 width: units.gu(40)
2929 height: units.gu(70)
3030
31− MenuItemFactory {
31+ IndicatorMenuItemFactory {
3232 id: factory
3333 menuModel: UnityMenuModel {}
3434 }
@@ -51,7 +51,7 @@ Item {
5151 }
5252
5353 UT.UnityTestCase {
54− name: "MenuItemFactory"
54+ name: "IndicatorMenuItemFactory"
5555 when: windowShown
5656
5757 property QtObject menuData: QtObject {
tests/qmltests/Panel/IndicatorTest.qml → tests/qmltests/Panel/PanelTest.qmlrenamed−5
@@ -15,11 +15,6 @@
1515 */
1616
1717import QtQuick 2.4
18−import QtQuick.Layouts 1.1
19−import QtTest 1.0
20−import "../../../qml/Panel"
21−import Ubuntu.Components 1.3
22−import Unity.Test 0.1 as UT
2318import Unity.Indicators 0.1 as Indicators
2419
2520Rectangle {
tests/qmltests/Panel/tst_MenuContent.qml+14−2
@@ -20,7 +20,7 @@ import Unity.Test 0.1 as UT
2020import "../../../qml/Panel"
2121import Unity.Indicators 0.1 as Indicators
2222
23−IndicatorTest {
23+PanelTest {
2424 id: root
2525 width: units.gu(40)
2626 height: units.gu(70)
@@ -31,8 +31,20 @@ IndicatorTest {
3131
3232 MenuContent {
3333 id: menuContent
34− indicatorsModel: root.indicatorsModel
34+ model: root.indicatorsModel
3535 height: parent.height - 50
36+
37+ pageDelegate: Rectangle {
38+
39+ function reset() {
40+ }
41+
42+ Binding {
43+ target: parent
44+ property: "objectName"
45+ value: modelData ? modelData.identifier : ""
46+ }
47+ }
3648 }
3749
3850 Rectangle {
tests/qmltests/Panel/tst_Panel.qml+384−103
@@ -19,26 +19,42 @@ import QtQuick.Layouts 1.1
1919import QtTest 1.0
2020import Unity.Test 0.1
2121import Ubuntu.Components 1.3
22+import Ubuntu.Components.ListItems 1.3 as ListItem
2223import Unity.Application 0.1
23−import Unity.Indicators 0.1 as Indicators
24+import QMenuModel 0.1
2425import Ubuntu.Telephony 0.1 as Telephony
26+import AccountsService 0.1
27+import Unity.InputInfo 0.1
2528import "../../../qml/Panel"
2629import "../../../qml/Components/PanelState"
30+import "../Stage"
31+import ".."
2732
28−IndicatorTest {
33+PanelTest {
2934 id: root
30− width: units.gu(100)
35+ width: units.gu(120)
3136 height: units.gu(71)
3237 color: "black"
3338
34− Component.onCompleted: theme.name = "Ubuntu.Components.Themes.SuruDark"
39+ Binding {
40+ target: QuickUtils
41+ property: "keyboardAttached"
42+ value: keyboardAttached.checked
43+ }
3544
36− SurfaceManager {}
45+ SurfaceManager { id: sMgr }
46+ ApplicationMenuDataLoader {
47+ id: appMenuData
48+ surfaceManager: sMgr
49+ }
3750
38− Binding {
39− target: mouseEmulation
40− property: "checked"
41− value: !windowControlsCB.checked
51+ Component.onCompleted: {
52+ theme.name = "Ubuntu.Components.Themes.SuruDark"
53+ }
54+
55+ Rectangle {
56+ anchors.fill: parent
57+ color: "darkgrey"
4258 }
4359
4460 RowLayout {
@@ -61,12 +77,23 @@ IndicatorTest {
6177 Panel {
6278 id: panel
6379 anchors.fill: parent
64− indicators {
65− width: parent.width > units.gu(60) ? units.gu(40) : parent.width
66− indicatorsModel: root.indicatorsModel
80+ mode: modeSelector.model[modeSelector.selectedIndex]
81+
82+ indicatorMenuWidth: parent.width > units.gu(60) ? units.gu(40) : parent.width
83+ applicationMenuWidth: parent.width > units.gu(60) ? units.gu(40) : parent.width
84+
85+ applicationMenus {
86+ model: UnityMenuModel {
87+ modelData: appMenuData.generateTestData(5, 4, 2, 3, "menu")
88+ }
89+
90+ hides: [ panel.indicators ]
6791 }
6892
69− property real panelAndSeparatorHeight: panel.indicators.minimizedPanelHeight
93+ indicators {
94+ model: root.indicatorsModel
95+ hides: [ panel.applicationMenus ]
96+ }
7097 }
7198 }
7299 }
@@ -75,6 +102,18 @@ IndicatorTest {
75102 Layout.alignment: Qt.AlignTop
76103 Layout.fillWidth: false
77104
105+ ListItem.ItemSelector {
106+ id: modeSelector
107+ anchors { left: parent.left; right: parent.right }
108+ activeFocusOnPress: false
109+ text: "Mode"
110+ model: ["staged", "windowed" ]
111+ onSelectedIndexChanged: {
112+ panel.mode = model[selectedIndex];
113+ keyboardAttached.checked = panel.mode == "windowed"
114+ }
115+ }
116+
78117 Button {
79118 Layout.fillWidth: true
80119 text: panel.indicators.shown ? "Hide" : "Show"
@@ -109,25 +148,22 @@ IndicatorTest {
109148 Layout.fillWidth: true
110149 CheckBox {
111150 id: windowControlsCB
112− onClicked: PanelState.buttonsVisible = checked
151+ onClicked: PanelState.decorationsVisible = checked
113152 }
114153 Label {
115− text: "Show window controls"
154+ text: "Show window decorations"
155+ color: "white"
116156 }
117157 }
118158
119159 RowLayout {
120160 Layout.fillWidth: true
121161 CheckBox {
122− onClicked: {
123− if (checked)
124− PanelState.title = "Fake window title"
125− else
126− PanelState.title = ""
127− }
162+ onClicked: PanelState.title = checked ? "Fake window title" : ""
128163 }
129164 Label {
130165 text: "Show fake window title"
166+ color: "white"
131167 }
132168 }
133169
@@ -137,6 +173,12 @@ IndicatorTest {
137173 color: "black"
138174 }
139175
176+ Rectangle {
177+ Layout.preferredHeight: units.dp(1);
178+ Layout.fillWidth: true;
179+ color: "black"
180+ }
181+
140182 Repeater {
141183 model: root.originalModelData
142184 RowLayout {
@@ -147,6 +189,7 @@ IndicatorTest {
147189 Label {
148190 Layout.fillWidth: true
149191 text: modelData["identifier"]
192+ color: "white"
150193 }
151194
152195 CheckBox {
@@ -155,6 +198,7 @@ IndicatorTest {
155198 }
156199 Label {
157200 text: "visible"
201+ color: "white"
158202 }
159203 }
160204 }
@@ -165,7 +209,22 @@ IndicatorTest {
165209 color: "black"
166210 }
167211
168− MouseTouchEmulationCheckbox { id: mouseEmulation }
212+ MouseTouchEmulationCheckbox {
213+ id: mouseEmulation
214+ color: "white"
215+ checked: panel.mode == "staged"
216+ }
217+
218+ RowLayout {
219+ Layout.fillWidth: true
220+ CheckBox {
221+ id: keyboardAttached
222+ }
223+ Label {
224+ text: "Keyboard Attached"
225+ color: "white"
226+ }
227+ }
169228 }
170229 }
171230
@@ -191,22 +250,31 @@ IndicatorTest {
191250 }
192251
193252 function init() {
253+ panel.mode = "staged";
254+ mouseEmulation.checked = true;
194255 panel.fullscreenMode = false;
195256 callManager.foregroundCall = null;
196257
197− panel.indicators.hide();
198− // Wait for animation to complete
199− tryCompare(panel.indicators.hideAnimation, "running", false);
258+ PanelState.title = "";
259+ PanelState.decorationsVisible = false;
200260
201261 // Wait for the indicators to get into position.
202262 // (switches between normal and fullscreen modes are animated)
203− var indicatorArea = findChild(panel, "indicatorArea");
204− tryCompare(indicatorArea, "y", 0);
263+ var panelArea = findChild(panel, "panelArea");
264+ tryCompare(panelArea, "y", 0);
205265
206266 backgroundPressedSpy.clear();
207267 compare(backgroundPressedSpy.valid, true);
208268 windowControlButtonsSpy.clear();
209269 compare(windowControlButtonsSpy.valid, true);
270+
271+ waitForRendering(panel);
272+ }
273+
274+ function cleanup() {
275+ panel.indicators.hide();
276+ panel.applicationMenus.hide();
277+ waitForAllAnimationToComplete("initial");
210278 }
211279
212280 function get_indicator_item(index) {
@@ -216,8 +284,28 @@ IndicatorTest {
216284 return indicatorItem;
217285 }
218286
287+ function waitForAllAnimationToComplete(state) {
288+
289+ waitUntilTransitionsEnd(panel);
290+ tryCompare(panel.indicators.hideAnimation, "running", false);
291+ tryCompare(panel.indicators.showAnimation, "running", false);
292+ tryCompare(panel.indicators, "state", state);
293+
294+ for (var i = 0; i < root.originalModelData.length; i++) {
295+ var indicatorItem = get_indicator_item(i);
296+
297+ var itemState = findInvisibleChild(indicatorItem, "indicatorItemState");
298+ verify(itemState !== null);
299+
300+ waitUntilTransitionsEnd(itemState);
301+ }
302+
303+ tryCompare(panel.applicationMenus.hideAnimation, "running", false);
304+ tryCompare(panel.applicationMenus, "state", state);
305+ }
306+
219307 function pullDownIndicatorsMenu() {
220− var showDragHandle = findChild(panel, "showDragHandle");
308+ var showDragHandle = findChild(panel.indicators, "showDragHandle");
221309 touchFlick(showDragHandle,
222310 showDragHandle.width / 2,
223311 showDragHandle.height / 2,
@@ -226,16 +314,22 @@ IndicatorTest {
226314 tryCompare(panel.indicators, "fullyOpened", true);
227315 }
228316
229− function test_drag_show_data() {
317+ function pullDownApplicationsMenu() {
318+ var showDragHandle = findChild(panel.applicationMenus, "showDragHandle");
319+ touchFlick(showDragHandle,
320+ showDragHandle.width / 2,
321+ showDragHandle.height / 2,
322+ showDragHandle.width / 2,
323+ showDragHandle.height / 2 + (showDragHandle.autoCompleteDragThreshold * 1.1));
324+ tryCompare(panel.applicationMenus, "fullyOpened", true);
325+ }
326+
327+ function test_drag_indicator_item_down_shows_menu_data() {
230328 return [
231− { tag: "pinned", fullscreen: false, call: null,
232− indicatorY: 0 },
233− { tag: "fullscreen", fullscreen: true, call: null,
234− indicatorY: -panel.panelAndSeparatorHeight },
235− { tag: "pinned-callActive", fullscreen: false, call: phoneCall,
236− indicatorY: 0},
237− { tag: "fullscreen-callActive", fullscreen: true, call: phoneCall,
238− indicatorY: -panel.panelAndSeparatorHeight }
329+ { tag: "pinned", fullscreen: false, call: null, y: 0 },
330+ { tag: "fullscreen", fullscreen: true, call: null, y: -panel.minimizedPanelHeight },
331+ { tag: "pinned-callActive", fullscreen: false, call: phoneCall, y: 0},
332+ { tag: "fullscreen-callActive", fullscreen: true, call: phoneCall, y: -panel.minimizedPanelHeight }
239333 ];
240334 }
241335
@@ -243,24 +337,26 @@ IndicatorTest {
243337 // indicators, first by running the hint animation, then after dragging down will
244338 // expose more of the panel, binding it to the selected indicator and opening it's menu.
245339 // Tested from first Y pixel to check for swipe from offscreen.
246− function test_drag_show(data) {
340+ function test_drag_indicator_item_down_shows_menu(data) {
247341 panel.fullscreenMode = data.fullscreen;
248342 callManager.foregroundCall = data.call;
249343
250− var indicatorRow = findChild(panel.indicators, "indicatorItemRow");
251− verify(indicatorRow !== null);
344+ var panelRow = findChild(panel.indicators, "panelItemRow");
345+ verify(panelRow !== null);
252346
253347 var menuContent = findChild(panel.indicators, "menuContent");
254348 verify(menuContent !== null);
255349
256− var indicatorArea = findChild(panel, "indicatorArea");
257− verify(indicatorArea !== null);
350+ var panelArea = findChild(panel, "panelArea");
351+ verify(panelArea !== null);
258352
259353 // Wait for the indicators to get into position.
260354 // (switches between normal and fullscreen modes are animated)
261− tryCompareFunction(function() { return indicatorArea.y }, data.indicatorY);
355+ tryCompareFunction(function() { return panelArea.y }, data.y);
262356
263357 for (var i = 0; i < root.originalModelData.length; i++) {
358+ waitForAllAnimationToComplete("initial");
359+
264360 var indicatorItem = get_indicator_item(i);
265361
266362 var startXPosition = panel.mapFromItem(indicatorItem, indicatorItem.width / 2, 0).x;
@@ -268,7 +364,7 @@ IndicatorTest {
268364 touchFlick(panel,
269365 startXPosition, 0,
270366 startXPosition, panel.height,
271− true /* beginTouch */, false /* endTouch */, units.gu(5), 15);
367+ true /* beginTouch */, false /* endTouch */, 1, 15);
272368
273369 // Indicators height should follow the drag, and therefore increase accordingly.
274370 // They should be at least half-way through the screen
@@ -278,105 +374,152 @@ IndicatorTest {
278374
279375 touchRelease(panel, startXPosition, panel.height);
280376
281− compare(indicatorRow.currentItemIndex, i, "Indicator item should be activated at position " + i);
377+ tryCompare(panelRow, "currentItemIndex", i, undefined, "Indicator item should be activated at position " + i);
282378 compare(menuContent.currentMenuIndex, i, "Menu conetent should be activated for item at position " + i);
283379
284380 // init for next indicatorItem
285381 panel.indicators.hide();
286− tryCompare(panel.indicators.hideAnimation, "running", false);
287− tryCompare(panel.indicators, "state", "initial");
288382 }
289383 }
290384
291− function test_drag_hide_data() {
385+ function test_drag_panel_handle_up_hides_menu_data() {
292386 return [
293− { tag: "pinned", fullscreen: false, call: null,
294− indicatorY: 0 },
295− { tag: "fullscreen", fullscreen: true, call: null,
296− indicatorY: -panel.panelAndSeparatorHeight },
297− { tag: "pinned-callActive", fullscreen: false, call: phoneCall,
298− indicatorY: 0},
299− { tag: "fullscreen-callActive", fullscreen: true, call: phoneCall,
300− indicatorY: -panel.panelAndSeparatorHeight }
387+ { tag: "indicators-pinned", section: panel.indicators, fullscreen: false, call: null },
388+ { tag: "indicators-fullscreen", section: panel.indicators, fullscreen: true, call: null },
389+ { tag: "indicators-pinned-callActive", section: panel.indicators, fullscreen: false, call: phoneCall },
390+ { tag: "indicators-fullscreen-callActive", section: panel.indicators, fullscreen: true, call: phoneCall },
391+ { tag: "appMenus-pinned", section: panel.applicationMenus, fullscreen: false, call: null },
392+ { tag: "appMenus-fullscreen", section: panel.applicationMenus, fullscreen: true, call: null },
393+ { tag: "appMenus-pinned-callActive", section: panel.applicationMenus, fullscreen: false, call: phoneCall },
394+ { tag: "appMenus-fullscreen-callActive", section: panel.applicationMenus, fullscreen: true, call: phoneCall }
301395 ];
302396 }
303397
304398 // Dragging the shown indicators up from bottom of panel will hide the indicators
305399 // Tested from last Y pixel to check for swipe from offscreen.
306− function test_drag_hide(data) {
400+ function test_drag_panel_handle_up_hides_menu(data) {
307401 panel.fullscreenMode = data.fullscreen;
308402 callManager.foregroundCall = data.call;
309403
310− var indicatorRow = findChild(panel.indicators, "indicatorItemRow");
311− verify(indicatorRow !== null);
404+ var panelRow = findChild(data.section, "panelItemRow");
405+ verify(panelRow !== null);
312406
313− var menuContent = findChild(panel.indicators, "menuContent");
314− verify(menuContent !== null);
315−
316− var indicatorArea = findChild(panel, "indicatorArea");
317− verify(indicatorArea !== null);
407+ var panelArea = findChild(panel, "panelArea");
408+ verify(panelArea !== null);
318409
319410 // Wait for the indicators to get into position.
320411 // (switches between normal and fullscreen modes are animated)
321− tryCompareFunction(function() { return indicatorArea.y }, data.indicatorY);
412+ tryCompareFunction(function() { return panelArea.y }, data.fullscreen ? -panel.minimizedPanelHeight : 0);
322413
323− panel.indicators.show();
324− tryCompare(panel.indicators.showAnimation, "running", false);
325− tryCompare(panel.indicators, "unitProgress", 1);
414+ data.section.show();
415+ tryCompare(data.section.showAnimation, "running", false);
416+ tryCompare(data.section, "unitProgress", 1);
326417
327− touchFlick(panel.indicators,
328− panel.indicators.width / 2, panel.height,
329− panel.indicators.width / 2, 0,
418+ touchFlick(data.section,
419+ data.section.width / 2, panel.height,
420+ data.section.width / 2, 0,
330421 true /* beginTouch */, false /* endTouch */, units.gu(5), 15);
331422
332423 // Indicators height should follow the drag, and therefore increase accordingly.
333424 // They should be at least half-way through the screen
334425 tryCompareFunction(
335− function() {return panel.indicators.height <= panel.height * 0.5},
426+ function() {return data.section.height <= panel.height * 0.5},
336427 true);
337428
338− touchRelease(panel.indicators, panel.indicators.width / 2, 0);
429+ touchRelease(data.section, data.section.width / 2, 0);
339430
340− tryCompare(panel.indicators.hideAnimation, "running", true);
341− tryCompare(panel.indicators.hideAnimation, "running", false);
342− tryCompare(panel.indicators, "state", "initial");
431+ tryCompare(data.section.hideAnimation, "running", true);
432+ tryCompare(data.section.hideAnimation, "running", false);
433+ tryCompare(data.section, "state", "initial");
343434 }
344435
345436 function test_hint_data() {
346437 return [
347− { tag: "normal", fullscreen: false, call: null, hintExpected: true},
348− { tag: "fullscreen", fullscreen: true, call: null, hintExpected: false},
349− { tag: "call hint", fullscreen: false, call: phoneCall, hintExpected: false},
438+ { tag: "indicators-normal", section: panel.indicators, fullscreen: false, call: null, hintExpected: true},
439+ { tag: "indicators-fullscreen", section: panel.indicators, fullscreen: true, call: null, hintExpected: false},
440+ { tag: "indicators-callActive", section: panel.indicators, fullscreen: false, call: phoneCall, hintExpected: false},
441+ { tag: "appMenus-normal", section: panel.applicationMenus, fullscreen: false, call: null, hintExpected: true},
442+ { tag: "appMenus-fullscreen", section: panel.applicationMenus, fullscreen: true, call: null, hintExpected: false},
443+ { tag: "appMenus-callActive", section: panel.applicationMenus, fullscreen: false, call: phoneCall, hintExpected: false},
350444 ];
351445 }
352446
353447 function test_hint(data) {
448+ PanelState.title = "Fake Title"
354449 panel.fullscreenMode = data.fullscreen;
355450 callManager.foregroundCall = data.call;
356451
357452 if (data.fullscreen) {
358453 // Wait for the indicators to get into position.
359454 // (switches between normal and fullscreen modes are animated)
360− var indicatorArea = findChild(panel, "indicatorArea");
361− tryCompare(indicatorArea, "y", -panel.panelHeight);
455+ var panelArea = findChild(panel, "panelArea");
456+ tryCompare(panelArea, "y", -panel.minimizedPanelHeight);
362457 }
363458
364− var indicatorItem = get_indicator_item(0);
365− var mappedPosition = root.mapFromItem(indicatorItem, indicatorItem.width / 2, indicatorItem.height / 2);
459+ var mappedPosition = root.mapFromItem(data.section,
460+ data.section.barWidth / 2, data.section.minimizedPanelHeight / 2);
461+
462+ touchPress(panel, mappedPosition.x, panel.minimizedPanelHeight / 2);
366463
367− touchPress(panel, mappedPosition.x, panel.indicators.minimizedPanelHeight / 2);
464+ var showDragHandle = findChild(data.section, "showDragHandle")
465+ var hintingAnimation = findInvisibleChild(showDragHandle, "hintingAnimation");
466+ verify(hintingAnimation);
368467
369− // Give some time for a hint animation to change things, if any
370− wait(500);
468+ compare(hintingAnimation.running, data.hintExpected)
469+ tryCompare(hintingAnimation, "running", false); // wait till animation completes
371470
372471 // no hint animation when fullscreen
373− compare(panel.indicators.fullyClosed, !data.hintExpected, "Indicator should be fully closed");
374− compare(panel.indicators.partiallyOpened, data.hintExpected, "Indicator should be partialy opened");
375− compare(panel.indicators.fullyOpened, false, "Indicator should not be fully opened");
472+ compare(data.section.partiallyOpened, data.hintExpected, "Indicator should be partialy opened");
473+ compare(data.section.fullyOpened, false, "Indicator should not be fully opened");
376474
377475 touchRelease(panel, mappedPosition.x, panel.minimizedPanelHeight / 2);
378476 }
379477
478+ function test_drag_applicationMenu_down_shows_menu_data() {
479+ return [
480+ { tag: "normal", fullscreen: false, call: null, hintExpected: true},
481+ { tag: "fullscreen", fullscreen: true, call: null, hintExpected: false},
482+ { tag: "callActive", fullscreen: false, call: phoneCall, hintExpected: false}
483+ ];
484+ }
485+
486+ // Dragging the application menu will gradually expose the
487+ // menus, first by running the hint animation, then after dragging down will
488+ // expose more of the panel. Releasing the touch will complete the show.
489+ function test_drag_applicationMenu_down_shows_menu(data) {
490+ PanelState.title = "Fake Title";
491+ panel.fullscreenMode = data.fullscreen;
492+ callManager.foregroundCall = data.call;
493+
494+ var panelRow = findChild(panel.indicators, "panelItemRow");
495+ verify(panelRow !== null);
496+
497+ var menuContent = findChild(panel.indicators, "menuContent");
498+ verify(menuContent !== null);
499+
500+ var panelArea = findChild(panel, "panelArea");
501+ verify(panelArea !== null);
502+
503+ // Wait for the indicators to get into position.
504+ // (switches between normal and fullscreen modes are animated)
505+ tryCompareFunction(function() { return panelArea.y }, data.fullscreen ? -panel.minimizedPanelHeight : 0);
506+
507+ touchFlick(panel,
508+ units.gu(1), 0,
509+ units.gu(1), panel.height,
510+ true /* beginTouch */, false /* endTouch */, units.gu(5), 15);
511+
512+ // Indicators height should follow the drag, and therefore increase accordingly.
513+ // They should be at least half-way through the screen
514+ tryCompareFunction(
515+ function() {return panel.applicationMenus.height >= panel.height * 0.5},
516+ true);
517+
518+ touchRelease(panel, units.gu(1), panel.height);
519+
520+ tryCompare(panel.applicationMenus, "fullyOpened", true)
521+ }
522+
380523 /* Checks that no input reaches items behind the indicator bar.
381524 Ie., the indicator bar should eat all input events that hit it.
382525 */
@@ -385,20 +528,20 @@ IndicatorTest {
385528 // that it doesn't have a "weak spot" from where taps pass through.
386529 var numTaps = 5;
387530 var stepLength = (panel.width / (numTaps + 1));
388− var tapY = panel.indicators.minimizedPanelHeight / 2;
531+ var tapY = panel.minimizedPanelHeight / 2;
389532 for (var i = 1; i <= numTaps; ++i) {
390533 tap(panel, stepLength * i, tapY);
391534 tryCompare(panel.indicators, "fullyClosed", true);
392535 }
393536 }
394537
395− function test_darkenedAreaEatsAllEvents() {
538+ function test_darkenedAreaEatsAllIndicatorEvents() {
396539
397540 // The center of the area not covered by the indicators menu
398541 // Ie, the visible darkened area behind the menu
399542 var touchPosX = (panel.width - panel.indicators.width) / 2
400− var touchPosY = panel.indicators.minimizedPanelHeight +
401− ((panel.height - panel.indicators.minimizedPanelHeight) / 2)
543+ var touchPosY = panel.minimizedPanelHeight +
544+ ((panel.height - panel.minimizedPanelHeight) / 2)
402545
403546 // input goes through while the indicators menu is closed
404547 tryCompare(panel.indicators, "fullyClosed", true);
@@ -434,6 +577,50 @@ IndicatorTest {
434577 compare(backgroundPressedSpy.count, 2);
435578 }
436579
580+ function test_darkenedAreaEatsAllApplicationMenuEvents() {
581+ PanelState.title = "Fake Title"
582+
583+ // The center of the area not covered by the indicators menu
584+ // Ie, the visible darkened area behind the menu
585+ var touchPosX = panel.applicationMenus.width + (panel.width - panel.applicationMenus.width) / 2
586+ var touchPosY = panel.minimizedPanelHeight +
587+ ((panel.height - panel.minimizedPanelHeight) / 2)
588+
589+ // input goes through while the indicators menu is closed
590+ tryCompare(panel.applicationMenus, "fullyClosed", true);
591+ compare(backgroundPressedSpy.count, 0);
592+ tap(panel, touchPosX, touchPosY);
593+ compare(backgroundPressedSpy.count, 2);
594+
595+ pullDownApplicationsMenu();
596+
597+ // Darkened area eats input when the indicators menu is fully opened
598+ tap(panel, touchPosX, touchPosY);
599+ compare(backgroundPressedSpy.count, 2);
600+ backgroundPressedSpy.clear();
601+
602+ // And should continue to eat inpunt until the indicators menu is fully closed
603+ wait(10);
604+ while (!panel.applicationMenus.fullyClosed) {
605+ tap(panel, touchPosX, touchPosY);
606+
607+ // it could have got fully closed during the tap
608+ // so we have to double check here
609+ if (!panel.applicationMenus.fullyClosed) {
610+ compare(backgroundPressedSpy.count, 0);
611+ }
612+
613+ // let the animation go a bit further
614+ wait(50);
615+ }
616+
617+ // Now that's fully closed, input should go through again
618+ backgroundPressedSpy.clear();
619+ tap(panel, touchPosX, touchPosY);
620+ compare(backgroundPressedSpy.count, 2);
621+ }
622+
623+
437624 /*
438625 Regression test for https://bugs.launchpad.net/ubuntu/+source/unity8/+bug/1439318
439626 When the panel is in fullscreen mode and the user taps near the top edge,
@@ -441,14 +628,12 @@ IndicatorTest {
441628 panel.
442629 */
443630 function test_tapNearTopEdgeWithPanelInFullscreenMode() {
444− var indicatorArea = findChild(panel, "indicatorArea");
445− verify(indicatorArea);
446− var panelPriv = findInvisibleChild(panel, "panelPriv");
447− verify(panelPriv);
631+ var panelArea = findChild(panel, "panelArea");
632+ verify(panelArea);
448633
449634 panel.fullscreenMode = true;
450635 // wait until if finishes hiding itself
451− tryCompare(indicatorArea, "y", -panelPriv.indicatorHeight);
636+ tryCompare(panelArea, "y", -panel.minimizedPanelHeight);
452637
453638 compare(panel.indicators.shown, false);
454639 verify(panel.indicators.fullyClosed);
@@ -479,7 +664,7 @@ IndicatorTest {
479664
480665 mouseClick(panel.indicators,
481666 panel.indicators.width / 2,
482− panel.indicators.minimizedPanelHeight / 2);
667+ panel.minimizedPanelHeight / 2);
483668
484669 compare(panel.indicators.shown, false);
485670 verify(panel.indicators.fullyClosed);
@@ -494,7 +679,7 @@ IndicatorTest {
494679
495680 mouseClick(panel.indicators,
496681 panel.indicators.width / 2,
497− panel.indicators.minimizedPanelHeight / 2);
682+ panel.minimizedPanelHeight / 2);
498683
499684 compare(panel.indicators.shown, true);
500685 tryCompare(panel.indicators, "fullyOpened", true);
@@ -510,13 +695,109 @@ IndicatorTest {
510695
511696 // https://bugs.launchpad.net/ubuntu/+source/unity8/+bug/1611959
512697 function test_windowControlButtonsFittsLaw() {
513− var buttonsVisible = PanelState.buttonsVisible;
514− PanelState.buttonsVisible = true;
698+ panel.mode = "windowed";
699+ var windowControlArea = findChild(panel, "windowControlArea");
700+ verify(windowControlArea, "Window control area should have been created in windowed mode")
701+
702+ PanelState.decorationsVisible = true;
515703 // click in very topleft corner and verify the close button got clicked too
516704 mouseMove(panel, 0, 0);
517705 mouseClick(panel, 0, 0, undefined /*button*/, undefined /*modifiers*/, 100 /*short delay*/);
518706 compare(windowControlButtonsSpy.count, 1);
519− PanelState.buttonsVisible = buttonsVisible;
707+ }
708+
709+ function test_hidingKeyboardIndicator_data() {
710+ return [
711+ { tag: "No keyboard, no keymap", keyboard: false, keymaps: [], hidden: true },
712+ { tag: "No keyboard, one keymap", keyboard: false, keymaps: ["us"], hidden: true },
713+ { tag: "No keyboard, 2 keymaps", keyboard: false, keymaps: ["us", "cs"], hidden: true },
714+ { tag: "Keyboard, no keymap", keyboard: true, keymaps: [], hidden: true },
715+ { tag: "Keyboard, one keymap", keyboard: true, keymaps: ["us"], hidden: true },
716+ { tag: "Keyboard, 2 keymaps", keyboard: true, keymaps: ["us", "cs"], hidden: false }
717+ ];
718+ }
719+
720+ function test_hidingKeyboardIndicator(data) {
721+ var item = findChild(panel, "indicator-keyboard-panelItem");
722+ AccountsService.keymaps = data.keymaps;
723+ if (data.keyboard) {
724+ MockInputDeviceBackend.addMockDevice("/indicator_kbd0", InputInfo.Keyboard);
725+ } else {
726+ MockInputDeviceBackend.removeDevice("/indicator_kbd0");
727+ }
728+
729+ compare(item.hidden, data.hidden);
730+ }
731+
732+ function test_visibleIndicators_data() {
733+ return [
734+ { visible: [true, false, true, false, true, true, false, true] },
735+ { visible: [false, false, false, false, false, false, true, false] }
736+ ];
737+ }
738+
739+ function test_visibleIndicators(data) {
740+ for (var i = 0; i < data.visible.length; i++) {
741+ var visible = data.visible[i];
742+ root.setIndicatorVisible(i, visible);
743+
744+ var dataItem = findChild(panel, root.originalModelData[i]["identifier"] + "-panelItem");
745+ tryCompare(dataItem, "opacity", visible ? 1.0 : 0.0);
746+ tryCompareFunction(function() { return dataItem.width > 0.0; }, visible);
747+ }
748+
749+ panel.indicators.show();
750+ tryCompare(panel.indicators.showAnimation, "running", false);
751+ tryCompare(panel.indicators, "unitProgress", 1);
752+
753+ for (var i = 0; i < data.visible.length; i++) {
754+ root.setIndicatorVisible(i, data.visible[i]);
755+
756+ var dataItem = findChild(panel, root.originalModelData[i]["identifier"] + "-panelItem");
757+ tryCompareFunction(function() { return dataItem.opacity > 0.0; }, true);
758+ tryCompareFunction(function() { return dataItem.width > 0.0; }, true);
759+ }
760+ }
761+
762+ function test_stagedApplicationMenuBarShowOnMouseHover() {
763+ PanelState.title = "Fake Title";
764+ panel.mode = "staged";
765+ mouseEmulation.checked = false;
766+
767+ var appTitle = findChild(panel.applicationMenus, "panelTitle"); verify(appTitle);
768+ var appMenuRow = findChild(panel.applicationMenus, "panelRow"); verify(appMenuRow);
769+ var appMenuBar = findChild(panel, "menuBar"); verify(appMenuBar);
770+
771+ tryCompare(appTitle, "visible", true, undefined, "App title should be visible");
772+ tryCompare(appMenuBar, "visible", false, undefined, "App menu bar should not be visible");
773+
774+ mouseMove(panel, panel.width/2, panel.panelHeight);
775+
776+ tryCompare(appTitle, "visible", false, undefined, "App title should not be visible on mouse hover");
777+ tryCompare(appMenuBar, "visible", true, undefined, "App menu bar should be visible on mouse hover");
778+ }
779+
780+ function test_windowedApplicationMenuShowOnMouseHoverWhenDecorationsShown() {
781+ PanelState.title = "Fake Title";
782+ panel.mode = "windowed";
783+ mouseEmulation.checked = false;
784+
785+ var appTitle = findChild(panel.applicationMenus, "panelTitle"); verify(appTitle);
786+ var appMenuRow = findChild(panel.applicationMenus, "panelRow"); verify(appMenuRow);
787+ var appMenuBar = findChild(panel, "menuBar"); verify(appMenuBar);
788+
789+ tryCompare(appTitle, "visible", true, undefined, "App title should be visible");
790+ tryCompare(appMenuBar, "visible", false, undefined, "App menu bar should not be visible");
791+
792+ mouseMove(panel, panel.width/2, panel.panelHeight);
793+
794+ tryCompare(appTitle, "visible", true, undefined, "App title should still be visible on mouse hover when panel decorations are not visible");
795+ tryCompare(appMenuBar, "visible", false, undefined, "App menu bar should be visible on mouse hover when panel decorations are not visible");
796+
797+ PanelState.decorationsVisible = true;
798+
799+ tryCompare(appTitle, "visible", false, undefined, "App title should still be visible on mouse hover when panel decorations are visible");
800+ tryCompare(appMenuBar, "visible", true, undefined, "App menu bar should be visible on mouse hover when panel decorations not visible");
520801 }
521802 }
522803}
tests/qmltests/Panel/tst_IndicatorsBar.qml → tests/qmltests/Panel/tst_PanelBar.qmlrenamed+24−33
@@ -21,8 +21,9 @@ import "../../../qml/Panel"
2121import Ubuntu.Components 1.3
2222import Unity.Test 0.1 as UT
2323import Unity.Indicators 0.1 as Indicators
24+import "../../../qml/Panel/Indicators"
2425
25−IndicatorTest {
26+PanelTest {
2627 id: root
2728 width: units.gu(100)
2829 height: units.gu(40)
@@ -43,14 +44,31 @@ IndicatorTest {
4344 anchors.fill: indicatorsBar
4445 }
4546
46− IndicatorsBar {
47+ PanelBar {
4748 id: indicatorsBar
4849 height: expanded ? units.gu(7) : units.gu(3)
4950 width: units.gu(widthSlider.value)
5051 anchors.centerIn: parent
51− indicatorsModel: root.indicatorsModel
52+ model: root.indicatorsModel
5253 interactive: expanded && height === units.gu(7)
5354
55+ rowItemDelegate: Item {
56+ property int ownIndex: index
57+ objectName: model.identifier + "-panelItem"
58+
59+ implicitWidth: indicatorsBar.expanded ? units.gu(5) : units.gu(3)
60+ height: parent.height
61+
62+ Rectangle {
63+ anchors {
64+ fill: parent
65+ margins: 2
66+ }
67+ color: "red"
68+ Label { anchors.centerIn: parent; text: ownIndex }
69+ }
70+ }
71+
5472 Behavior on height {
5573 NumberAnimation {
5674 id: heightAnimation
@@ -171,7 +189,7 @@ IndicatorTest {
171189 var dataItem = findChild(indicatorsBar, root.originalModelData[lastItemIndex]["identifier"] + "-panelItem");
172190 verify(dataItem !== null);
173191
174− var row = findChild(indicatorsBar, "indicatorItemRow");
192+ var row = findChild(indicatorsBar, "panelItemRow");
175193 // test will not work without these conditions
176194 verify(row.width >= indicatorsBar.width + dataItem.width);
177195
@@ -198,36 +216,9 @@ IndicatorTest {
198216 skip("Out of bounds");
199217 }
200218 mouseClick(dataItem);
201− verify(dataItem.selected === true);
202− }
203−
204− function test_visibleIndicators_data() {
205− return [
206− { visible: [true, false, true, false, true, true, false, true] },
207− { visible: [false, false, false, false, false, false, true, false] }
208− ];
209− }
210−
211− function test_visibleIndicators(data) {
212− for (var i = 0; i < data.visible.length; i++) {
213− var visible = data.visible[i];
214− root.setIndicatorVisible(i, visible);
215219
216− var dataItem = findChild(indicatorsBar, root.originalModelData[i]["identifier"] + "-panelItem");
217− tryCompare(dataItem, "opacity", visible ? 1.0 : 0.0);
218− tryCompareFunction(function() { return dataItem.width > 0.0; }, visible);
219− }
220−
221− indicatorsBar.expanded = true;
222− wait_for_expansion_to_settle();
223−
224− for (var i = 0; i < data.visible.length; i++) {
225− root.setIndicatorVisible(i, data.visible[i]);
226−
227− var dataItem = findChild(indicatorsBar, root.originalModelData[i]["identifier"] + "-panelItem");
228− tryCompareFunction(function() { return dataItem.opacity > 0.0; }, true);
229− tryCompareFunction(function() { return dataItem.width > 0.0; }, true);
230− }
220+ var row = findChild(indicatorsBar, "panelItemRow");
221+ compare(dataItem, row.currentItem, "Item should be selected");
231222 }
232223
233224 // Rough test that resizing the IndicatorsBar has the items reposition correctly
tests/qmltests/Panel/tst_IndicatorItemRow.qml → tests/qmltests/Panel/tst_PanelItemRow.qmlrenamed+36−32
@@ -17,14 +17,13 @@
1717import QtQuick 2.4
1818import QtQuick.Layouts 1.1
1919import QtTest 1.0
20−import "../../../qml/Panel"
2120import Ubuntu.Components 1.3
2221import Unity.Test 0.1 as UT
23−import Unity.Indicators 0.1 as Indicators
2422import AccountsService 0.1
2523import Unity.InputInfo 0.1
24+import "../../../qml/Panel"
2625
27−IndicatorTest {
26+PanelTest {
2827 id: root
2928 width: units.gu(120)
3029 height: units.gu(40)
@@ -45,11 +44,11 @@ IndicatorTest {
4544 anchors.fill: indicatorsRow
4645 }
4746
48− IndicatorItemRow {
47+ PanelItemRow {
4948 id: indicatorsRow
5049 height: expanded ? units.gu(7) : units.gu(3)
5150 anchors.centerIn: parent
52− indicatorsModel: root.indicatorsModel
51+ model: root.indicatorsModel
5352 enableLateralChanges: ma.pressed
5453
5554 Behavior on height {
@@ -59,6 +58,23 @@ IndicatorTest {
5958 }
6059 }
6160
61+ delegate: Item {
62+ property int ownIndex: index
63+ objectName: model.identifier + "-panelItem"
64+
65+ implicitWidth: indicatorsRow.expanded ? units.gu(6) : units.gu(3)
66+ height: parent.height
67+
68+ Rectangle {
69+ anchors {
70+ fill: parent
71+ margins: 2
72+ }
73+ color: "red"
74+ Label { anchors.centerIn: parent; text: ownIndex }
75+ }
76+ }
77+
6278 MouseArea {
6379 id: ma
6480 anchors.fill: parent
@@ -116,7 +132,7 @@ IndicatorTest {
116132 }
117133
118134 UT.UnityTestCase {
119− name: "IndicatorItemRow"
135+ name: "PanelItemRow"
120136 when: windowShown
121137
122138 function init() {
@@ -161,25 +177,36 @@ IndicatorTest {
161177 for (i = data.remove.length-1; i >= 0; i--) {
162178 removeIndicator(data.remove[i]);
163179 }
180+ wait(50);
164181
165182 // test removals
166183 for (i = 0; i < root.originalModelData.length; i++) {
167− item = findChild(indicatorsRow, root.originalModelData[i]["identifier"] + "-panelItem");
184+ item = findChild(indicatorsRow, root.originalModelData[i]["identifier"] + "-panelItem", 0);
185+ console.log(item, i, root.originalModelData[i]["identifier"])
168186
169− verify(data.remove.indexOf(i) !== -1 ? (item === null) : (item !== null));
187+ if (data.remove.indexOf(i) !== -1) {
188+ verify(item === null, "removed item should not be present");
189+ } else {
190+ verify(item !== null, "items not removed item should still be present");
191+ }
170192 }
193+ console.log("2")
171194
172195 // test insertion
173196 for (i = 0; i < data.remove.length; i++) {
174197 insertIndicator(data.remove[i]);
175198 }
176199
200+ console.log("3")
201+
177202 for (i = 0; i < root.originalModelData.length; i++) {
178− item = findChild(indicatorsRow, root.originalModelData[i]["identifier"] + "-panelItem");
203+ item = findChild(indicatorsRow, root.originalModelData[i]["identifier"] + "-panelItem", 100);
179204 verify(item);
180205
181206 compare(item.ownIndex, i, "Item at incorrect index");
182207 }
208+
209+ console.log("4")
183210 }
184211
185212 function test_validCurrentItem_data() {
@@ -287,28 +314,5 @@ IndicatorTest {
287314 // should go back to 0
288315 tryCompare(findChild(indicatorsRow, "highlight"), "highlightCenterOffset", 0);
289316 }
290−
291− function test_hidingKeyboardIndicator_data() {
292− return [
293− { tag: "No keyboard, no keymap", keyboard: false, keymaps: [], hidden: true },
294− { tag: "No keyboard, one keymap", keyboard: false, keymaps: ["us"], hidden: true },
295− { tag: "No keyboard, 2 keymaps", keyboard: false, keymaps: ["us", "cs"], hidden: true },
296− { tag: "Keyboard, no keymap", keyboard: true, keymaps: [], hidden: true },
297− { tag: "Keyboard, one keymap", keyboard: true, keymaps: ["us"], hidden: true },
298− { tag: "Keyboard, 2 keymaps", keyboard: true, keymaps: ["us", "cs"], hidden: false }
299− ];
300− }
301−
302− function test_hidingKeyboardIndicator(data) {
303− var item = findChild(indicatorsRow, "indicator-keyboard-panelItem");
304− AccountsService.keymaps = data.keymaps;
305− if (data.keyboard) {
306− MockInputDeviceBackend.addMockDevice("/indicator_kbd0", InputInfo.Keyboard);
307− } else {
308− MockInputDeviceBackend.removeDevice("/indicator_kbd0");
309− }
310−
311− compare(item.hidden, data.hidden);
312− }
313317 }
314318}
tests/qmltests/Panel/tst_IndicatorsMenu.qml → tests/qmltests/Panel/tst_PanelMenu.qmlrenamed+32−14
@@ -20,9 +20,8 @@ import QtTest 1.0
2020import "../../../qml/Panel"
2121import Ubuntu.Components 1.3
2222import Unity.Test 0.1 as UT
23−import Unity.Indicators 0.1 as Indicators
2423
25−IndicatorTest {
24+PanelTest {
2625 id: root
2726 width: units.gu(80)
2827 height: units.gu(71)
@@ -48,7 +47,7 @@ IndicatorTest {
4847 id: itemArea
4948 color: "blue"
5049
51− IndicatorsMenu {
50+ PanelMenu {
5251 id: indicatorsMenu
5352 width: units.gu(40)
5453 anchors {
@@ -58,8 +57,25 @@ IndicatorTest {
5857 minimizedPanelHeight: units.gu(3)
5958 expandedPanelHeight: units.gu(7)
6059 openedHeight: parent.height
61− indicatorsModel: root.indicatorsModel
60+ model: root.indicatorsModel
6261 shown: false
62+
63+ rowItemDelegate: Item {
64+ property int ownIndex: index
65+ objectName: model.identifier + "-panelItem"
66+
67+ implicitWidth: indicatorsMenu.expanded ? units.gu(5) : units.gu(3)
68+ height: parent.height
69+
70+ Rectangle {
71+ anchors {
72+ fill: parent
73+ margins: 2
74+ }
75+ color: "red"
76+ Label { anchors.centerIn: parent; text: ownIndex }
77+ }
78+ }
6379 }
6480 }
6581
@@ -111,7 +127,7 @@ IndicatorTest {
111127
112128 UT.UnityTestCase {
113129 id: testCase
114− name: "IndicatorsMenu"
130+ name: "PanelMenu"
115131 when: windowShown
116132
117133 function init() {
@@ -206,8 +222,8 @@ IndicatorTest {
206222 // tests swiping on an indicator item activates the correct item.
207223 function test_swipeForCurrentItem()
208224 {
209− var indicatorItemRow = findChild(indicatorsMenu, "indicatorItemRow");
210− verify(indicatorItemRow !== null);
225+ var panelItemRow = findChild(indicatorsMenu, "panelItemRow");
226+ verify(panelItemRow !== null);
211227
212228 for (var i = 0; i < root.originalModelData.length; i++) {
213229 var indicatorItem = get_indicator_item(i);
@@ -219,10 +235,10 @@ IndicatorTest {
219235 mappedPosition.x, indicatorsMenu.openedHeight / 2,
220236 true /* beginTouch */, false /* endTouch */);
221237
222− tryCompare(indicatorItemRow, "currentItem", indicatorItem, undefined /*timeout, default */,
238+ tryCompare(panelItemRow, "currentItem", indicatorItem, undefined /*timeout, default */,
223239 "Incorrect item activated at position " + i);
224240
225− touchFlick(indicatorItemRow,
241+ touchFlick(panelItemRow,
226242 mappedPosition.x, indicatorsMenu.openedHeight / 2,
227243 mappedPosition.x, mappedPosition.y,
228244 false /* beginTouch */, true /* endTouch */);
@@ -239,8 +255,8 @@ IndicatorTest {
239255 indicatorsMenu.verticalVelocityThreshold = 0;
240256 verify(root.originalModelData.length >= 2);
241257
242− var indicatorItemRow = findChild(indicatorsMenu, "indicatorItemRow");
243− verify(indicatorItemRow !== null);
258+ var panelItemRow = findChild(indicatorsMenu, "panelItemRow");
259+ verify(panelItemRow !== null);
244260
245261 // Get the first indicator
246262 var firstItem = get_indicator_item(0);
@@ -252,12 +268,14 @@ IndicatorTest {
252268 firstItemMappedPosition.x, indicatorsMenu.minimizedPanelHeight * 2,
253269 true /* beginTouch */, false /* endTouch */);
254270
255− tryCompare(indicatorItemRow, "currentItem", firstItem)
271+ tryCompare(panelItemRow, "currentItem", firstItem)
256272
257273 // next time position will have moved.
258274 var nextItem = get_indicator_item(1);
259275 var nextItemMappedPositionX = firstItemMappedPosition.x + firstItem.width;
260276
277+ firstItemMappedPosition = indicatorsMenu.mapFromItem(firstItem, firstItem.width/2, firstItem.height/2);
278+
261279 // 1) Flick mouse down to bottom
262280 touchFlick(indicatorsMenu,
263281 firstItemMappedPosition.x, indicatorsMenu.minimizedPanelHeight * 2,
@@ -265,10 +283,10 @@ IndicatorTest {
265283 false /* beginTouch */, false /* endTouch */,
266284 units.gu(50) /* speed */, 5 /* iterations */); // more samples needed for accurate velocity
267285
268− tryCompare(indicatorItemRow, "currentItem", firstItem, undefined /* timeout, default */,
286+ tryCompare(panelItemRow, "currentItem", firstItem, undefined /* timeout, default */,
269287 "First indicator should still be the current item");
270288 // after waiting in the same spot with touch down, it should update to the next item.
271− tryCompare(indicatorItemRow, "currentItem", nextItem);
289+ tryCompare(panelItemRow, "currentItem", nextItem);
272290
273291 touchRelease(indicatorsMenu, nextItemMappedPositionX, indicatorsMenu.openedHeight / 3);
274292 }
tests/qmltests/Panel/tst_IndicatorPage.qml → tests/qmltests/Panel/tst_PanelMenuPage.qmlrenamed+53−47
@@ -16,42 +16,46 @@
1616
1717import QtQuick 2.4
1818import QtTest 1.0
19+import Ubuntu.Components 1.3
1920import Unity.Test 0.1 as UT
20−import Unity.Indicators 0.1 as Indicators
21+import QMenuModel 0.1
2122import Ubuntu.Settings.Menus 0.1 as Menus
2223import "../../../qml/Panel"
24+import "../../../qml/Panel/Indicators"
2325
2426Item {
2527 id: testView
2628 width: units.gu(40)
2729 height: units.gu(70)
2830
29− IndicatorPage {
31+ UnityMenuModel {
32+ id: unityMenuModel
33+ modelData: fullMenuData
34+ }
35+
36+ PanelMenuPage {
3037 id: page
3138 anchors.fill: parent
3239
33− identifier: "test-indicator"
34− busName: "com.caninical.indicator.test"
35− actionsObjectPath: "/com/canonical/indicator/test"
36− menuObjectPath: "/com/canonical/indicator/test"
40+ menuModel: unityMenuModel
41+ submenuIndex: 0
42+
43+ factory: Object {
44+ function load(model) {
45+ return standardMenuComponent;
46+ }
47+
48+ Component {
49+ id: standardMenuComponent
3750
38− factory {
39− _userMap: {
40− "default": {
41− "com.canonical.indicator.test" : testMenu
51+ Menus.StandardMenu {
52+ signal menuSelected
53+ signal menuDeselected
4254 }
4355 }
4456 }
4557 }
4658
47− Component {
48− id: testMenu
49− Menus.StandardMenu {
50− signal menuSelected
51− signal menuDeselected
52− }
53− }
54−
5559 property var fullMenuData: [{
5660 "rowData": { // 1
5761 "label": "root",
@@ -126,43 +130,43 @@ Item {
126130 "submenu": []
127131 }]; // end row 1
128132
129− function initializeMenuData(data) {
130− Indicators.UnityMenuModelCache.setCachedModelData("/com/canonical/indicator/test",
131− data);
132− }
133−
134133 UT.UnityTestCase {
135134 name: "IndicatorPage"
136135
137136 function init() {
138− initializeMenuData([]);
137+ page.submenuIndex = 0;
138+ unityMenuModel.modelData = [];
139+ tryCompareFunction(function() { return page.currentPage == null }, true);
139140 }
140141
141− function test_reloadData() {
142− var mainMenu = findChild(page, "mainMenu");
143−
144− tryCompare(mainMenu, "count", 0);
142+ function test_loadData() {
143+ unityMenuModel.modelData = fullMenuData;
145144
146− initializeMenuData(fullMenuData);
147− tryCompare(mainMenu, "count", 3);
145+ tryCompareFunction(function() { return page.currentPage != null }, true);
146+ var listView = findChild(page.currentPage, "listView", 50);
147+ verify(listView);
148+ tryCompare(listView, "count", 3);
148149
149− initializeMenuData([]);
150− tryCompare(mainMenu, "count", 0);
150+ unityMenuModel.modelData = [];
151+ tryCompareFunction(function() { return page.currentPage == null }, true);
151152 }
152153
153− function test_traverse_rootMenuType_data() {
154− return [
155− { tag: "Incorrect", rootMenuType: "com.canonical.indicator", expectedCount: 0},
156− { tag: "Correct", rootMenuType: "com.canonical.indicator.root", expectedCount: 3},
154+ function test_traverse_submenuIndex_data() {
155+ return [,
156+ { tag: "Correct", submenuIndex: 0, expectedCount: 3},
157+ { tag: "Incorrect", submenuIndex: 1, expectedCount: 0}
157158 ]
158159 }
159160
160− function test_traverse_rootMenuType(data) {
161− page.rootMenuType = data.rootMenuType;
162− initializeMenuData(fullMenuData);
161+ function test_traverse_submenuIndex(data) {
162+ page.submenuIndex = data.submenuIndex;
163+ unityMenuModel.modelData = fullMenuData;
163164
164− var mainMenu = findChild(page, "mainMenu");
165− tryCompare(mainMenu, "count", data.expectedCount);
165+ tryCompareFunction(function() { return page.currentPage != null }, data.expectedCount > 0);
166+ if (data.expectedCount > 0) {
167+ var listView = findChild(page.currentPage, "listView", 50);
168+ tryCompare(listView, "count", data.expectedCount);
169+ }
166170 }
167171
168172 function test_remove_selected_item_data() {
@@ -173,20 +177,22 @@ Item {
173177 }
174178
175179 function test_remove_selected_item(data) {
176− var mainMenu = findChild(page, "mainMenu");
177− initializeMenuData(fullMenuData);
180+ unityMenuModel.modelData = fullMenuData;
181+ var listView = findChild(page.currentPage, "listView", 50);
178182
179183 var menuId = "menu"+data.remove
180− var menu = findChild(page, menuId);
184+ var menu = findChild(listView, menuId);
185+ verify(menu);
181186
182187 menu.menuSelected();
183− compare(mainMenu.currentIndex, data.remove, "Incorrect index selected");
184− mainMenu.model.removeRow(data.remove);
188+ compare(listView.currentIndex, data.remove, "Incorrect index selected");
189+ listView.model.removeRow(data.remove);
185190
186− compare(mainMenu.currentIndex, -1, "Current index should be reset after current item removal");
191+ compare(listView.currentIndex, -1, "Current index should be reset after current item removal");
187192
188193 // now make sure selecting a new menu works.
189194 var menu1 = findChild(page, "menu1");
195+ verify(menu1);
190196 menu1.menuSelected();
191197 compare(menu1.selected, true, "Item not selected");
192198 }
tests/qmltests/Stage/tst_DecoratedWindow.qml+40−5
@@ -19,10 +19,13 @@ import QtTest 1.0
1919import Unity.Test 0.1 as UT
2020import ".."
2121import "../../../qml/Components"
22+import "../../../qml/Components/PanelState"
2223import "../../../qml/Stage"
2324import Ubuntu.Components 1.3
2425import Ubuntu.Components.ListItems 1.3 as ListItem
2526import Unity.Application 0.1
27+import Unity.ApplicationMenu 0.1
28+import Unity.Indicators 0.1 as Indicators
2629
2730Rectangle {
2831 color: "red"
@@ -30,8 +33,25 @@ Rectangle {
3033 width: fakeShell.shortestDimension + controls.width
3134 height: fakeShell.longestDimension
3235
36+ Component.onCompleted: {
37+ QuickUtils.keyboardAttached = true;
38+ theme.name = "Ubuntu.Components.Themes.SuruDark"
39+ }
40+
3341 property QtObject fakeApplication: null
3442
43+ Binding {
44+ target: UT.MouseTouchAdaptor
45+ property: "enabled"
46+ value: false
47+ }
48+
49+ SurfaceManager { id: sMgr }
50+ ApplicationMenuDataLoader {
51+ id: appMenuData
52+ surfaceManager: sMgr
53+ }
54+
3555 Item {
3656 id: fakeShell
3757
@@ -49,6 +69,11 @@ Rectangle {
4969
5070 rotation: orientationAngle
5171
72+ WindowResizeArea {
73+ target: loader
74+ borderThickness: units.gu(2)
75+ }
76+
5277 Loader {
5378 id: loader
5479 property int windowedX: units.gu(1)
@@ -64,12 +89,21 @@ Rectangle {
6489 property bool itemDestroyed: false
6590
6691 sourceComponent: DecoratedWindow {
92+ id: decoratedWindow
93+
6794 anchors.fill: parent
6895 application: fakeApplication
6996 requestedWidth: loader.windowedWidth
7097 requestedHeight: loader.windowedHeight
98+ active: true
7199 surface: fakeApplication && fakeApplication.surfaceList.count > 0 ? fakeApplication.surfaceList.get(0) : null
72100
101+ Binding {
102+ target: PanelState
103+ property: "focusedPersistentSurfaceId"
104+ value: decoratedWindow.surface ? decoratedWindow.surface.persistentId : "x"
105+ }
106+
73107 Component.onDestruction: {
74108 loader.itemDestroyed = true;
75109 }
@@ -80,16 +114,12 @@ Rectangle {
80114
81115 Rectangle { anchors.fill: parent; color: "green"; opacity: 1 }
82116 }
83− WindowResizeArea {
84− target: loader
85− borderThickness: units.gu(2)
86− }
87117 }
88118
89119
90120 Rectangle {
91121 id: controls
92− color: "white"
122+ color: theme.palette.normal.background
93123 width: units.gu(30)
94124 anchors {
95125 top: parent.top
@@ -100,6 +130,11 @@ Rectangle {
100130 Column {
101131 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
102132 spacing: units.gu(1)
133+ Button {
134+ id: loadWidthDialerApp
135+ text: "Load with dialer-app"
136+ onClicked: { testCase.restartWithApp("dialer-app"); }
137+ }
103138 Button {
104139 id: loadWithWeatherApp
105140 text: "Load with ubuntu-weather-app"
tests/qmltests/Stage/tst_DesktopStage.qml+9−45
@@ -19,6 +19,8 @@ import QtTest 1.0
1919import Ubuntu.Components 1.3
2020import Ubuntu.Components.ListItems 1.3
2121import Unity.Application 0.1
22+import Unity.ApplicationMenu 0.1
23+import Unity.Indicators 0.1 as Indicators
2224import Unity.Test 0.1
2325import Utils 0.1
2426import WindowManager 1.0
@@ -42,6 +44,7 @@ Item {
4244 }
4345
4446 Component.onCompleted: {
47+ QuickUtils.keyboardAttached = true;
4548 theme.name = "Ubuntu.Components.Themes.SuruDark";
4649 resetGeometry();
4750 }
@@ -58,6 +61,11 @@ Item {
5861 }
5962
6063 SurfaceManager { id: sMgr }
64+ ApplicationMenuDataLoader {
65+ id: appMenuData
66+ surfaceManager: sMgr
67+ }
68+
6169 TopLevelWindowModel {
6270 id: topSurfaceList
6371 applicationManager: ApplicationManager
@@ -94,13 +102,6 @@ Item {
94102 mode: "windowed"
95103 }
96104 }
97−
98− MouseArea {
99− id: clickThroughTester
100− anchors.fill: stageLoader.item
101− acceptedButtons: Qt.AllButtons
102− hoverEnabled: true
103− }
104105 }
105106
106107 Rectangle {
@@ -152,11 +153,6 @@ Item {
152153 }
153154 }
154155
155− SignalSpy {
156− id: mouseEaterSpy
157− target: clickThroughTester
158− }
159−
160156 StageTestCase {
161157 id: testCase
162158 name: "DesktopStage"
@@ -197,8 +193,6 @@ Item {
197193
198194 stageLoader.active = true;
199195 tryCompare(stageLoader, "status", Loader.Ready);
200−
201− mouseEaterSpy.clear();
202196 }
203197
204198 function maximizeAppDelegate(appDelegate) {
@@ -709,43 +703,13 @@ Item {
709703
710704 var posBefore = Qt.point(appDelegate.x, appDelegate.y);
711705
712− mousePress(appDelegate, appDelegate.width / 2, units.gu(1), data.button);
713− mouseMove(appDelegate, appDelegate.width / 2, -units.gu(100), undefined /* delay */, data.button);
706+ mouseDrag(appDelegate, appDelegate.width / 2, units.gu(1), 0, appDelegate.height / 2, data.button, Qt.NoModifier, 200)
714707
715708 var posAfter = Qt.point(appDelegate.x, appDelegate.y);
716709
717710 tryCompareFunction(function(){return posBefore == posAfter;}, data.button !== Qt.LeftButton ? true : false);
718711 }
719712
720− function test_eatWindowDecorationMouseEvents_data() {
721− return [
722− {tag: "left mouse click", signalName: "clicked", button: Qt.LeftButton },
723− {tag: "right mouse click", signalName: "clicked", button: Qt.RightButton },
724− {tag: "middle mouse click", signalName: "clicked", button: Qt.MiddleButton },
725− {tag: "mouse wheel", signalName: "wheel", button: Qt.MiddleButton },
726− {tag: "double click (RMB)", signalName: "doubleClicked", button: Qt.RightButton },
727− ]
728− }
729−
730− function test_eatWindowDecorationMouseEvents(data) {
731− var dialerAppDelegate = startApplication("dialer-app");
732− verify(dialerAppDelegate);
733− var decoration = findChild(dialerAppDelegate, "appWindowDecoration");
734− verify(decoration);
735−
736− mouseEaterSpy.signalName = data.signalName;
737− if (data.signalName === "wheel") {
738− mouseWheel(decoration, decoration.width/2, decoration.height/2, 20, 20);
739− } else if (data.signalName === "clicked") {
740− mouseClick(decoration, decoration.width/2, decoration.height/2, data.button);
741− } else {
742− mouseDoubleClick(decoration, decoration.width/2, decoration.height/2, data.button);
743− tryCompare(dialerAppDelegate, "maximized", false);
744− }
745−
746− tryCompare(mouseEaterSpy, "count", 0);
747− }
748−
749713 // regression test for https://bugs.launchpad.net/ubuntu/+source/unity8/+bug/1627281
750714 function test_doubleTapToMaximizeWindow() {
751715 var dialerAppDelegate = startApplication("dialer-app");
tests/qmltests/Stage/tst_PhoneStage.qml+5
@@ -32,6 +32,11 @@ Item {
3232 property var greeter: { fullyShown: true }
3333
3434 SurfaceManager { id: sMgr }
35+ ApplicationMenuDataLoader {
36+ id: appMenuData
37+ surfaceManager: sMgr
38+ }
39+
3540 Stage {
3641 id: stage
3742 anchors { fill: parent; rightMargin: units.gu(30) }
tests/qmltests/Stage/tst_TabletStage.qml+6
@@ -36,6 +36,11 @@ Rectangle {
3636 property var greeter: { fullyShown: true }
3737
3838 SurfaceManager { id: sMgr }
39+ ApplicationMenuDataLoader {
40+ id: appMenuData
41+ surfaceManager: sMgr
42+ }
43+
3944 Stage {
4045 id: stage
4146 anchors { fill: parent; rightMargin: units.gu(30) }
@@ -587,6 +592,7 @@ Rectangle {
587592
588593 function test_loadSideStageByDraggingFromMainStage() {
589594 sideStage.showNow();
595+ print("sidestage now shown. launching browser")
590596 var webbrowserSurfaceId = topSurfaceList.nextId;
591597 webbrowserCheckBox.checked = true;
592598 waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
tests/qmltests/Stage/tst_WindowDecoration.qmlnew file+244
@@ -0,0 +1,244 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtTest 1.0
19+import QtQuick.Layouts 1.1
20+import Ubuntu.Components 1.3
21+import Ubuntu.Components.ListItems 1.3
22+import Unity.Application 0.1
23+import Unity.Test 0.1
24+import Utils 0.1
25+import QMenuModel 0.1
26+
27+import ".."
28+import "../../../qml/Stage"
29+
30+Item {
31+ id: root
32+ width: units.gu(70)
33+ height: units.gu(50)
34+
35+ Component.onCompleted: {
36+ QuickUtils.keyboardAttached = true;
37+ theme.name = "Ubuntu.Components.Themes.SuruDark"
38+ }
39+
40+ Binding {
41+ target: MouseTouchAdaptor
42+ property: "enabled"
43+ value: false
44+ }
45+
46+ property QtObject application: QtObject {
47+ property string name: "webbrowser"
48+ }
49+
50+ SurfaceManager { id: sMgr }
51+ ApplicationMenuDataLoader {
52+ id: appMenuData
53+ surfaceManager: sMgr
54+ }
55+
56+ UnityMenuModel {
57+ id: menuBackend
58+ modelData: appMenuData.generateTestData(5, 3, 3, "menu")
59+ onActivated: log.text = "Activated " + action + "\n" + log.text
60+ }
61+
62+ Rectangle {
63+ anchors {
64+ left: parent.left
65+ right: parent.right
66+ bottom: parent.bottom
67+ margins: units.gu(1)
68+ }
69+ height: units.gu(20)
70+ border.width: 1
71+ border.color: "black"
72+
73+ TextArea {
74+ id: log
75+ anchors.fill: parent
76+ readOnly: true
77+ color: "black"
78+ }
79+ }
80+
81+ MouseArea {
82+ id: clickThroughTester
83+ anchors.fill: parent
84+ acceptedButtons: Qt.AllButtons
85+ hoverEnabled: true
86+ }
87+
88+ Loader {
89+ id: decorationLoader
90+ anchors { left: parent.left; right: parent.right; top: parent.top }
91+ anchors.margins: units.gu(1)
92+ height: units.gu(3)
93+
94+ property bool itemDestroyed: false
95+ sourceComponent: WindowDecoration {
96+ anchors.fill: parent
97+ title: "TestTitle - Doing something"
98+ active: true
99+ menu: menuBackend
100+
101+ onCloseClicked: { log.text = "Close\n" + log.text }
102+ onMinimizeClicked: { log.text = "Minimize\n" + log.text }
103+ onMaximizeClicked: { log.text = "Maximize\n" + log.text }
104+
105+ Component.onDestruction: {
106+ decorationLoader.itemDestroyed = true;
107+ }
108+ }
109+ }
110+
111+ SignalSpy {
112+ id: signalSpy
113+ target: decorationLoader.item
114+ }
115+
116+ SignalSpy {
117+ id: mouseEaterSpy
118+ target: clickThroughTester
119+ }
120+
121+ UnityTestCase {
122+ id: testCase
123+ name: "WindowDecoration"
124+ when: windowShown
125+
126+ property Item decoration: decorationLoader.status === Loader.Ready ? decorationLoader.item : null
127+
128+ function init() {
129+ tryCompareFunction(function() { return decoration !== null }, true);
130+ decoration.menu = menuBackend;
131+
132+ waitForRendering(decoration);
133+ }
134+
135+ function cleanup() {
136+ decorationLoader.itemDestroyed = false;
137+ decorationLoader.active = false;
138+
139+ tryCompare(decorationLoader, "status", Loader.Null);
140+ tryCompare(decorationLoader, "item", null);
141+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
142+ // actually took place. Likely because Loader waits until the next event loop
143+ // iteration to do its work. So to ensure the reload, we will wait until the
144+ // Shell instance gets destroyed.
145+ tryCompare(decorationLoader, "itemDestroyed", true);
146+
147+ decorationLoader.active = true;
148+ tryCompare(decorationLoader, "status", Loader.Ready);
149+
150+ signalSpy.clear();
151+ mouseEaterSpy.clear();
152+ }
153+
154+ function test_windowControlButtons_data() {
155+ return [ { tag: "close", controlName: "closeWindowButton", signal: "closeClicked"},
156+ { tag: "minimize", controlName: "minimizeWindowButton", signal: "minimizeClicked"},
157+ { tag: "maximize", controlName: "maximizeWindowButton", signal: "maximizeClicked"}];
158+ }
159+
160+ function test_windowControlButtons(data) {
161+ signalSpy.signalName = data.signal;
162+ var controlButton = findChild(decoration, data.controlName);
163+ verify(controlButton !== null);
164+
165+ mouseClick(controlButton, controlButton.width/2, controlButton.height/2);
166+ compare(signalSpy.count, 1);
167+ }
168+
169+ function test_titleRemainsWhenHoveringOnTitleBarWithNoMenu() {
170+ decoration.menu = undefined;
171+
172+ var menuLoader = findChild(decoration, "menuBarLoader");
173+ verify(menuLoader);
174+ mouseMove(menuLoader, menuLoader.width/2, menuLoader.height/2);
175+ wait(200);
176+
177+ var titleLabel = findChild(decoration, "windowDecorationTitle");
178+ verify(menuLoader);
179+
180+ compare(menuLoader.opacity, 0, "Menu should not show when present")
181+ compare(titleLabel.opacity, 1, "Title should always show when app menu not present")
182+ }
183+
184+ function test_menuShowsWhenHoveringOnTitleBar() {
185+ var menuLoader = findChild(decoration, "menuBarLoader");
186+ verify(menuLoader);
187+ mouseMove(menuLoader, menuLoader.width/2, menuLoader.height/2)
188+
189+ var titleLabel = findChild(decoration, "windowDecorationTitle");
190+ verify(menuLoader);
191+
192+ tryCompare(menuLoader, "opacity", 1);
193+ tryCompare(titleLabel, "opacity", 0);
194+
195+ mouseMove(menuLoader, menuLoader.width/2, menuLoader.height * 2);
196+
197+ tryCompare(menuLoader, "opacity", 0);
198+ tryCompare(titleLabel, "opacity", 1);
199+ }
200+
201+ function test_showMenuBarWithShortcutsOnLongAltPress() {
202+ var menuLoader = findChild(decoration, "menuBarLoader");
203+ verify(menuLoader);
204+
205+ var titleLabel = findChild(decoration, "windowDecorationTitle");
206+ verify(menuLoader);
207+
208+ var menuBar = findChild(decoration, "menuBar");
209+ verify(menuBar);
210+
211+ keyPress(Qt.Key_Alt, Qt.NoModifier);
212+ tryCompare(menuLoader, "opacity", 1);
213+ tryCompare(titleLabel, "opacity", 0);
214+
215+ keyRelease(Qt.Key_Alt, Qt.NoModifier);
216+ tryCompare(menuLoader, "opacity", 0);
217+ tryCompare(titleLabel, "opacity", 1);
218+ }
219+
220+ function test_eatMouseEvents_data() {
221+ return [
222+ {tag: "left mouse click", signalName: "clicked", button: Qt.LeftButton },
223+ {tag: "right mouse click", signalName: "clicked", button: Qt.RightButton },
224+ {tag: "middle mouse click", signalName: "clicked", button: Qt.MiddleButton },
225+ {tag: "mouse wheel", signalName: "wheel", button: Qt.MiddleButton },
226+ {tag: "double click (LMB)", signalName: "doubleClicked", button: Qt.LeftButton },
227+ {tag: "double click (RMB)", signalName: "doubleClicked", button: Qt.RightButton },
228+ ]
229+ }
230+
231+ function test_eatMouseEvents(data) {
232+ mouseEaterSpy.signalName = data.signalName;
233+ if (data.signalName === "wheel") {
234+ mouseWheel(decoration, decoration.width/2, decoration.height/2, 20, 20);
235+ } else if (data.signalName === "clicked") {
236+ mouseClick(decoration, decoration.width/2, decoration.height/2, data.button);
237+ } else {
238+ mouseDoubleClick(decoration, decoration.width/2, decoration.height/2, data.button);
239+ }
240+
241+ tryCompare(mouseEaterSpy, "count", 0);
242+ }
243+ }
244+}
tests/qmltests/tst_Shell.qml+34−5
@@ -24,6 +24,7 @@ import Ubuntu.Components 1.3
2424import Ubuntu.Components.ListItems 1.3 as ListItem
2525import Ubuntu.Telephony 0.1 as Telephony
2626import Unity.Application 0.1
27+import Unity.ApplicationMenu 0.1
2728import Unity.Connectivity 0.1
2829import Unity.Indicators 0.1
2930import Unity.Notifications 1.0
@@ -32,6 +33,7 @@ import Unity.Test 0.1
3233import Powerd 0.1
3334import Wizard 0.1 as Wizard
3435import Utils 0.1
36+import Unity.Indicators 0.1 as Indicators
3537
3638import "../../qml"
3739import "../../qml/Components"
@@ -51,12 +53,18 @@ Rectangle {
5153 shellLoader.active = true;
5254 }
5355
56+ ApplicationMenuDataLoader {
57+ id: appMenuData
58+ }
59+
5460 property var shell: shellLoader.item ? shellLoader.item : null
5561 onShellChanged: {
5662 if (shell) {
5763 topLevelSurfaceList = testCase.findInvisibleChild(shell, "topLevelSurfaceList");
64+ appMenuData.surfaceManager = testCase.findInvisibleChild(shell, "surfaceManager");
5865 } else {
5966 topLevelSurfaceList = null;
67+ appMenuData.surfaceManager = null;
6068 }
6169 }
6270
@@ -1878,21 +1886,21 @@ Rectangle {
18781886 var maximizeButton = findChild(appDelegate, "maximizeWindowButton");
18791887
18801888 tryCompare(appDelegate, "state", "normal");
1881− tryCompare(PanelState, "buttonsVisible", false)
1889+ tryCompare(PanelState, "decorationsVisible", false)
18821890
18831891 mouseClick(maximizeButton, maximizeButton.width / 2, maximizeButton.height / 2);
18841892 tryCompare(appDelegate, "state", "maximized");
1885− tryCompare(PanelState, "buttonsVisible", true)
1893+ tryCompare(PanelState, "decorationsVisible", true)
18861894
18871895 ApplicationManager.stopApplication(application.appId);
1888− tryCompare(PanelState, "buttonsVisible", false)
1896+ tryCompare(PanelState, "decorationsVisible", false)
18891897
18901898 // wait until all zombie surfaces are gone. As MirSurfaceItems hold references over them.
18911899 // They won't be gone until those surface items are destroyed.
18921900 tryCompareFunction(function() { return application.surfaceList.count }, 0);
18931901
18941902 ApplicationManager.startApplication(application.appId);
1895− tryCompare(PanelState, "buttonsVisible", true)
1903+ tryCompare(PanelState, "decorationsVisible", true)
18961904 }
18971905
18981906 function test_newAppHasValidGeometry() {
@@ -1913,6 +1921,7 @@ Rectangle {
19131921 var appRepeater = findChild(shell, "appRepeater")
19141922 var appDelegate = appRepeater.itemAt(0);
19151923 var panelButtons = findChild(shell, "panelWindowControlButtons")
1924+ verify(panelButtons)
19161925
19171926 tryCompare(appDelegate, "state", "normal");
19181927 tryCompare(panelButtons, "visible", false);
@@ -2547,7 +2556,7 @@ Rectangle {
25472556
25482557 tryCompare(appDelegate, "state", "maximized");
25492558
2550− mouseDrag(panel, panel.width/3, panel.height/2, 0, shell.height/3, Qt.LeftButton, Qt.NoModifier, 500);
2559+ mouseDrag(panel, panel.width/2, panel.height/2, 0, shell.height/3, Qt.LeftButton, Qt.NoModifier, 500);
25512560 tryCompare(appDelegate, "state", "restored");
25522561 }
25532562
@@ -2744,5 +2753,25 @@ Rectangle {
27442753 tryCompare(stage, "state", data.spreadEnabled ? "spread" : "staged");
27452754 keyRelease(Qt.Key_W, Qt.MetaModifier)
27462755 }
2756+
2757+ function test_panelTitleShowsWhenGreeterNotShown_data() {
2758+ return [
2759+ {tag: "phone" },
2760+ {tag: "tablet" },
2761+ {tag: "desktop" }
2762+ ]
2763+ }
2764+
2765+ function test_panelTitleShowsWhenGreeterNotShown(data) {
2766+ loadShell(data.tag);
2767+
2768+ var panel = findChild(shell, "panel"); verify(panel);
2769+ var panelTitle = findChild(panel.applicationMenus, "panelTitle"); verify(panelTitle);
2770+ compare(panelTitle.visible, false, "Panel title should not be visible when greeter is shown");
2771+
2772+ swipeAwayGreeter();
2773+
2774+ tryCompare(panelTitle, "visible", true, undefined, "Panel title should be visible when greeter not shown");
2775+ }
27472776 }
27482777}
tests/qmltests/tst_ShellWithPin.qml+3−1
@@ -474,7 +474,9 @@ Item {
474474
475475 ApplicationManager.requestFocusApplication("unity8-dash");
476476 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
477− tryCompare(panel.callHint, "visible", true);
477+ var callHint = findChild(panel, "callHint");
478+ tryCompare(callHint, "visible", true);
479+ wait(10000)
478480
479481 // simulate a callHint press, the real thing requires dialer: url support
480482 ApplicationManager.requestFocusApplication("dialer-app");