on xenial help sign in
Implement frontend support for running keyboard indicator
e01964aLukáš Tinkl
debian/control+2−1
@@ -130,7 +130,8 @@ Depends: dmz-cursor-theme,
130 unity8-private | unity-launcher-impl,
130 unity8-private | unity-launcher-impl,
131 ${misc:Depends},
131 ${misc:Depends},
132 ${shlibs:Depends},
132 ${shlibs:Depends},
133−Recommends: indicator-session,
133+Recommends: indicator-keyboard,
134+ indicator-session,
134 unity-scope-click,
135 unity-scope-click,
135 unity-scope-mediascanner2,
136 unity-scope-mediascanner2,
136Breaks: indicator-network (<< 0.5.1+14.10.20141014),
137Breaks: indicator-network (<< 0.5.1+14.10.20141014),
plugins/Unity/Indicators/indicatorsmanager.cpp+3−5
@@ -114,10 +114,6 @@ void IndicatorsManager::loadFile(const QFileInfo& file_info)
114 QSettings indicator_settings(file_info.absoluteFilePath(), QSettings::IniFormat, this);
114 QSettings indicator_settings(file_info.absoluteFilePath(), QSettings::IniFormat, this);
115 const QString name = indicator_settings.value(QStringLiteral("Indicator Service/Name")).toString();
115 const QString name = indicator_settings.value(QStringLiteral("Indicator Service/Name")).toString();
116
116
117− if (m_platform.isPC() && name == QLatin1String("indicator-keyboard")) {
118− return; // convergence: skip this indicator until it works in Mir
119− }
120−
121 auto iter = m_indicatorsData.constFind(name);
117 auto iter = m_indicatorsData.constFind(name);
122 if (iter != m_indicatorsData.constEnd())
118 if (iter != m_indicatorsData.constEnd())
123 {
119 {
@@ -285,11 +281,13 @@ Indicator::Ptr IndicatorsManager::indicator(const QString& indicator_name)
285
281
286 // convergence:
282 // convergence:
287 // 1) enable session indicator
283 // 1) enable session indicator
284+ // 2) enable keyboard indicator
288− // 2) on a PC, switch the battery/power indicator to desktop mode,
285+ // 3) on a PC, switch the battery/power indicator to desktop mode,
289 // can't control brightness for now and phone-on-desktop broken (FIXME)
286 // can't control brightness for now and phone-on-desktop broken (FIXME)
290 //
287 //
291 // The rest of the indicators respect their default profile (which is "phone", even on desktop PCs)
288 // The rest of the indicators respect their default profile (which is "phone", even on desktop PCs)
292 if ((new_indicator->identifier() == QStringLiteral("indicator-session"))
289 if ((new_indicator->identifier() == QStringLiteral("indicator-session"))
290+ || new_indicator->identifier() == QStringLiteral("indicator-keyboard")
293 || (new_indicator->identifier() == QStringLiteral("indicator-power") && m_platform.isPC())) {
291 || (new_indicator->identifier() == QStringLiteral("indicator-power") && m_platform.isPC())) {
294 new_indicator->setProfile(m_profile.replace(QStringLiteral("phone"), QStringLiteral("desktop")));
292 new_indicator->setProfile(m_profile.replace(QStringLiteral("phone"), QStringLiteral("desktop")));
295 } else {
293 } else {
qml/Components/KeymapSwitcher.qml+27−1
@@ -17,6 +17,7 @@
17import QtQuick 2.4
17import QtQuick 2.4
18import AccountsService 0.1
18import AccountsService 0.1
19import GlobalShortcut 1.0
19import GlobalShortcut 1.0
20+import QMenuModel 0.1
20import Unity.Application 0.1
21import Unity.Application 0.1
21
22
22QtObject {
23QtObject {
@@ -36,7 +37,8 @@ QtObject {
36
37
37 readonly property var keymaps: AccountsService.keymaps
38 readonly property var keymaps: AccountsService.keymaps
38 readonly property int keymapCount: keymaps.length
39 readonly property int keymapCount: keymaps.length
40+ // default keymap, either the one remembered by the indicator, or the 1st one selected by user
39− property int currentKeymapIndex: 0
41+ property int currentKeymapIndex: actionGroup.currentAction.valid ? actionGroup.currentAction.state : 0
40 readonly property string currentKeymap: keymaps[currentKeymapIndex]
42 readonly property string currentKeymap: keymaps[currentKeymapIndex]
41
43
42 function nextKeymap() {
44 function nextKeymap() {
@@ -62,4 +64,28 @@ QtObject {
62 property: "keymap"
64 property: "keymap"
63 value: root.currentKeymap
65 value: root.currentKeymap
64 }
66 }
67+
68+ // indicator
69+ property QDBusActionGroup actionGroup: QDBusActionGroup {
70+ busType: DBus.SessionBus
71+ busName: "com.canonical.indicator.keyboard"
72+ objectPath: "/com/canonical/indicator/keyboard"
73+
74+ property variant currentAction: action("current")
75+ property variant activeAction: action("active")
76+
77+ Component.onCompleted: actionGroup.start();
78+ }
79+
80+ onCurrentKeymapIndexChanged: {
81+ actionGroup.currentAction.updateState(currentKeymapIndex);
82+ }
83+
84+ readonly property int activeActionState: actionGroup.activeAction.valid ? actionGroup.activeAction.state : -1
85+
86+ onActiveActionStateChanged: {
87+ if (activeActionState != -1) {
88+ currentKeymapIndex = activeActionState;
89+ }
90+ }
65}
91}
qml/Panel/IndicatorItemRow.qml+13−2
@@ -18,6 +18,10 @@ import QtQuick 2.4
18import QtQuick.Window 2.2
18import QtQuick.Window 2.2
19import Ubuntu.Components 1.3
19import Ubuntu.Components 1.3
20
20
21+// for indicator-keyboard
22+import AccountsService 0.1
23+import Unity.InputInfo 0.1
24+
21Item {
25Item {
22 id: root
26 id: root
23 width: row.width
27 width: row.width
@@ -127,6 +131,11 @@ Item {
127 property bool forceAlignmentAnimationDisabled: false
131 property bool forceAlignmentAnimationDisabled: false
128 }
132 }
129
133
134+ InputDeviceModel {
135+ id: keyboardsModel
136+ deviceFilter: InputInfo.Keyboard
137+ }
138+
130 onCurrentItemChanged: {
139 onCurrentItemChanged: {
131 if (d.previousItem) {
140 if (d.previousItem) {
132 d.firstItemSwitch = false;
141 d.firstItemSwitch = false;
@@ -184,9 +193,11 @@ Item {
184
193
185 property int ownIndex: index
194 property int ownIndex: index
186 property bool overflow: row.width - x > overFlowWidth
195 property bool overflow: row.width - x > overFlowWidth
187− property bool hidden: !expanded && (overflow || !indicatorVisible || hideSessionIndicator)
196+ property bool hidden: !expanded && (overflow || !indicatorVisible || hideSessionIndicator || hideKeyboardIndicator)
188 // HACK for indicator-session
197 // HACK for indicator-session
189 readonly property bool hideSessionIndicator: identifier == "indicator-session" && Math.min(Screen.width, Screen.height) <= units.gu(60)
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)
190
201
191 height: row.height
202 height: row.height
192 expanded: root.expanded
203 expanded: root.expanded
@@ -202,7 +213,7 @@ Item {
202 NumberAnimation { duration: UbuntuAnimation.SnapDuration; easing: UbuntuAnimation.StandardEasing }
213 NumberAnimation { duration: UbuntuAnimation.SnapDuration; easing: UbuntuAnimation.StandardEasing }
203 }
214 }
204
215
205− width: ((expanded || indicatorVisible) && !hideSessionIndicator) ? implicitWidth : 0
216+ width: ((expanded || indicatorVisible) && !hideSessionIndicator && !hideKeyboardIndicator) ? implicitWidth : 0
206
217
207 Behavior on width {
218 Behavior on width {
208 NumberAnimation { duration: UbuntuAnimation.SnapDuration; easing: UbuntuAnimation.StandardEasing }
219 NumberAnimation { duration: UbuntuAnimation.SnapDuration; easing: UbuntuAnimation.StandardEasing }
qml/Panel/Indicators/MenuItemFactory.qml+65
@@ -985,6 +985,62 @@ Item {
985 }
985 }
986 }
986 }
987
987
988+ Component {
989+ id: keymapMenu;
990+
991+ // FIXME this should use a "radio button" menu, once we have it in the SDK
992+ ListItems.Empty {
993+ id: checkItem
994+ objectName: "keymapMenu"
995+ property QtObject menuData: null
996+ property int menuIndex: -1
997+ readonly property bool serverChecked: menuData && menuData.isToggled || false
998+
999+ enabled: menuData && menuData.sensitive || false
1000+ highlightWhenPressed: false
1001+ __acceptEvents: !serverChecked
1002+
1003+ CheckBox {
1004+ id: checkbox
1005+ __acceptEvents: !checkItem.serverChecked
1006+ anchors {
1007+ left: parent.left
1008+ leftMargin: checkItem.__contentsMargins
1009+ verticalCenter: parent.verticalCenter
1010+ }
1011+ }
1012+
1013+ ServerPropertySynchroniser {
1014+ objectName: "sync"
1015+ syncTimeout: Utils.Constants.indicatorValueTimeout
1016+
1017+ serverTarget: checkItem
1018+ serverProperty: "serverChecked"
1019+ userTarget: checkbox
1020+ userProperty: "checked"
1021+
1022+ onSyncTriggered: {
1023+ menuModel.activate(checkItem.menuIndex);
1024+ }
1025+ }
1026+
1027+ Label {
1028+ id: label
1029+ anchors {
1030+ left: checkbox.right
1031+ leftMargin: checkItem.__contentsMargins
1032+ right: parent.right
1033+ rightMargin: checkItem.__contentsMargins
1034+ verticalCenter: parent.verticalCenter
1035+ }
1036+ elide: Text.ElideRight
1037+ text: checkItem.menuData && checkItem.menuData.label || ""
1038+ }
1039+
1040+ onClicked: menuModel.activate(menuIndex);
1041+ }
1042+ }
1043+
988 function load(modelData, context) {
1044 function load(modelData, context) {
989 // tweak indicator-session items
1045 // tweak indicator-session items
990 if (context === "indicator-session") {
1046 if (context === "indicator-session") {
@@ -995,6 +1051,15 @@ Item {
995 }
1051 }
996 }
1052 }
997
1053
1054+ // specialize for indicator-keyboard
1055+ if (context === "indicator-keyboard") {
1056+ if (modelData.isRadio) {
1057+ return keymapMenu;
1058+ } else if (modelData.action === "indicator.map" || modelData.action === "indicator.chart") {
1059+ return null; // map and chart not available
1060+ }
1061+ }
1062+
998 if (modelData.type !== undefined && modelData.type !== "") {
1063 if (modelData.type !== undefined && modelData.type !== "") {
999 var component = undefined;
1064 var component = undefined;
1000
1065
tests/mocks/Unity/Indicators/IndicatorsModel.qml+41−1
@@ -16,11 +16,22 @@
16
16
17import QtQuick 2.4
17import QtQuick 2.4
18import Unity.Indicators 0.1 as Indicators
18import Unity.Indicators 0.1 as Indicators
19+import Unity.InputInfo 0.1
20+import AccountsService 0.1
19
21
20Indicators.FakeIndicatorsModel {
22Indicators.FakeIndicatorsModel {
21 id: root
23 id: root
22
24
23 property var originalModelData: [
25 property var originalModelData: [
26+ {
27+ "identifier": "indicator-keyboard",
28+ "indicatorProperties": {
29+ "enabled": true,
30+ "busName": "com.canonical.indicators.fake0",
31+ "menuObjectPath": "/com/canonical/indicators/fake0",
32+ "actionsObjectPath": "/com/canonical/indicators/fake0"
33+ }
34+ },
24 {
35 {
25 "identifier": "fake-indicator-bluetooth",
36 "identifier": "fake-indicator-bluetooth",
26 "indicatorProperties": {
37 "indicatorProperties": {
@@ -74,13 +85,38 @@ Indicators.FakeIndicatorsModel {
74 "menuObjectPath": "/com/canonical/indicators/fake6",
85 "menuObjectPath": "/com/canonical/indicators/fake6",
75 "actionsObjectPath": "/com/canonical/indicators/fake6"
86 "actionsObjectPath": "/com/canonical/indicators/fake6"
76 }
87 }
88+ },
89+ {
90+ "identifier": "fake-indicator-session",
91+ "indicatorProperties": {
92+ "enabled": true,
93+ "busName": "com.canonical.indicators.fake7",
94+ "menuObjectPath": "/com/canonical/indicators/fake7",
95+ "actionsObjectPath": "/com/canonical/indicators/fake7"
96+ }
77 }
97 }
78 ]
98 ]
79
99
100+ Component.onCompleted: {
101+ // init data for the fake indicator-keyboard
102+ MockInputDeviceBackend.addMockDevice("/indicator_kbd0", InputInfo.Keyboard);
103+ AccountsService.keymaps = ["us", "cs"];
104+ }
105+
106+ Component.onDestruction: {
107+ MockInputDeviceBackend.removeDevice("/indicator_kbd0");
108+ AccountsService.keymaps = ["us"];
109+ }
110+
80 function load(profile) {
111 function load(profile) {
81 unload();
112 unload();
82 root.modelData = originalModelData;
113 root.modelData = originalModelData;
83
114
115+ Indicators.UnityMenuModelCache.setCachedModelData("/com/canonical/indicators/fake0",
116+ getUnityMenuModelData("indicator-keyboard",
117+ "Czech (F)",
118+ "",
119+ [ "image://theme/input-keyboard-symbolic" ]));
84 Indicators.UnityMenuModelCache.setCachedModelData("/com/canonical/indicators/fake1",
120 Indicators.UnityMenuModelCache.setCachedModelData("/com/canonical/indicators/fake1",
85 getUnityMenuModelData("fake-indicator-bluetooth",
121 getUnityMenuModelData("fake-indicator-bluetooth",
86 "Bluetooth (F)",
122 "Bluetooth (F)",
@@ -111,6 +147,11 @@ Indicators.FakeIndicatorsModel {
111 "Upcoming Events (F)",
147 "Upcoming Events (F)",
112 "12:04",
148 "12:04",
113 []));
149 []));
150+ Indicators.UnityMenuModelCache.setCachedModelData("/com/canonical/indicators/fake7",
151+ getUnityMenuModelData("fake-indicator-session",
152+ "System (F)",
153+ "",
154+ ["image://theme/system-devices-panel"]));
114 }
155 }
115
156
116 function getUnityMenuModelData(identifier, title, label, icons) {
157 function getUnityMenuModelData(identifier, title, label, icons) {
@@ -173,6 +214,5 @@ Indicators.FakeIndicatorsModel {
173 break;
214 break;
174 }
215 }
175 }
216 }
176−
177 }
217 }
178}
218}
tests/qmltests/Panel/tst_IndicatorItemRow.qml+26−1
@@ -21,6 +21,8 @@ import "../../../qml/Panel"
21import Ubuntu.Components 1.3
21import Ubuntu.Components 1.3
22import Unity.Test 0.1 as UT
22import Unity.Test 0.1 as UT
23import Unity.Indicators 0.1 as Indicators
23import Unity.Indicators 0.1 as Indicators
24+import AccountsService 0.1
25+import Unity.InputInfo 0.1
24
26
25IndicatorTest {
27IndicatorTest {
26 id: root
28 id: root
@@ -194,7 +196,7 @@ IndicatorTest {
194 verify(dataItem !== null);
196 verify(dataItem !== null);
195
197
196 indicatorsRow.selectItemAt(dataItem.x + dataItem.width/2);
198 indicatorsRow.selectItemAt(dataItem.x + dataItem.width/2);
197− compare(indicatorsRow.currentItem, dataItem);
199+ tryCompare(indicatorsRow, "currentItem", dataItem);
198 }
200 }
199
201
200 // tests item default selection (no item at position X)
202 // tests item default selection (no item at position X)
@@ -285,5 +287,28 @@ IndicatorTest {
285 // should go back to 0
287 // should go back to 0
286 tryCompare(findChild(indicatorsRow, "highlight"), "highlightCenterOffset", 0);
288 tryCompare(findChild(indicatorsRow, "highlight"), "highlightCenterOffset", 0);
287 }
289 }
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+ }
288 }
313 }
289}
314}
tests/qmltests/Panel/tst_IndicatorsBar.qml+3−3
@@ -136,6 +136,7 @@ IndicatorTest {
136 }
136 }
137
137
138 function wait_for_expansion_to_settle() {
138 function wait_for_expansion_to_settle() {
139+ waitUntilTransitionsEnd(indicatorsBar);
139 tryCompare(heightAnimation, "running", false);
140 tryCompare(heightAnimation, "running", false);
140 wait(UbuntuAnimation.SnapDuration); // put a little extra wait in for things to settle
141 wait(UbuntuAnimation.SnapDuration); // put a little extra wait in for things to settle
141 }
142 }
@@ -151,7 +152,6 @@ IndicatorTest {
151 indicatorsBar.expanded = true;
152 indicatorsBar.expanded = true;
152 wait_for_expansion_to_settle();
153 wait_for_expansion_to_settle();
153
154
154−
155 // mappedPosition contained within mappedRect
155 // mappedPosition contained within mappedRect
156 tryCompareFunction(function() {
156 tryCompareFunction(function() {
157 var mappedRect = indicatorsBar.mapFromItem(dataItem, 0, 0, dataItem.width, dataItem.height);
157 var mappedRect = indicatorsBar.mapFromItem(dataItem, 0, 0, dataItem.width, dataItem.height);
@@ -203,8 +203,8 @@ IndicatorTest {
203
203
204 function test_visibleIndicators_data() {
204 function test_visibleIndicators_data() {
205 return [
205 return [
206− { visible: [true, false, true, false, true, true] },
206+ { visible: [true, false, true, false, true, true, false, true] },
207− { visible: [false, false, false, false, false, false] }
207+ { visible: [false, false, false, false, false, false, true, false] }
208 ];
208 ];
209 }
209 }
210
210
tests/qmltests/Panel/tst_IndicatorsMenu.qml+4−3
@@ -219,8 +219,8 @@ IndicatorTest {
219 mappedPosition.x, indicatorsMenu.openedHeight / 2,
219 mappedPosition.x, indicatorsMenu.openedHeight / 2,
220 true /* beginTouch */, false /* endTouch */);
220 true /* beginTouch */, false /* endTouch */);
221
221
222− compare(indicatorItemRow.currentItem, indicatorItem,
222+ tryCompare(indicatorItemRow, "currentItem", indicatorItem, undefined /*timeout, default */,
223− "Incorrect item activated at position " + i);
223+ "Incorrect item activated at position " + i);
224
224
225 touchFlick(indicatorItemRow,
225 touchFlick(indicatorItemRow,
226 mappedPosition.x, indicatorsMenu.openedHeight / 2,
226 mappedPosition.x, indicatorsMenu.openedHeight / 2,
@@ -265,7 +265,8 @@ IndicatorTest {
265 false /* beginTouch */, false /* endTouch */,
265 false /* beginTouch */, false /* endTouch */,
266 units.gu(50) /* speed */, 5 /* iterations */); // more samples needed for accurate velocity
266 units.gu(50) /* speed */, 5 /* iterations */); // more samples needed for accurate velocity
267
267
268+ tryCompare(indicatorItemRow, "currentItem", firstItem, undefined /* timeout, default */,
268− compare(indicatorItemRow.currentItem, firstItem, "First indicator should still be the current item");
269+ "First indicator should still be the current item");
269 // after waiting in the same spot with touch down, it should update to the next item.
270 // after waiting in the same spot with touch down, it should update to the next item.
270 tryCompare(indicatorItemRow, "currentItem", nextItem);
271 tryCompare(indicatorItemRow, "currentItem", nextItem);
271
272
tests/qmltests/tst_OrientedShell.qml+1
@@ -1117,6 +1117,7 @@ Rectangle {
1117
1117
1118 function test_attachRemoveInputDevices(data) {
1118 function test_attachRemoveInputDevices(data) {
1119 loadShell("mako")
1119 loadShell("mako")
1120+ MockInputDeviceBackend.removeDevice("/indicator_kbd0");
1120 var shell = findChild(orientedShell, "shell");
1121 var shell = findChild(orientedShell, "shell");
1121 var inputMethod = findChild(shell, "inputMethod");
1122 var inputMethod = findChild(shell, "inputMethod");
1122
1123