Lomiri
OrientedShell.qml
1 /*
2  * Copyright (C) 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 import QtQuick.Window 2.2
19 import Lomiri.InputInfo 0.1
20 import Lomiri.Session 0.1
21 import WindowManager 1.0
22 import Utils 0.1
23 import GSettings 1.0
24 import "Components"
25 import "Rotation"
26 // Workaround https://bugs.launchpad.net/lomiri/+source/lomiri/+bug/1473471
27 import Lomiri.Components 1.3
28 
29 Item {
30  id: root
31 
32  implicitWidth: units.gu(40)
33  implicitHeight: units.gu(71)
34 
35  property alias deviceConfiguration: _deviceConfiguration
36  property alias orientations: d.orientations
37  property bool lightIndicators: false
38 
39  onWidthChanged: calculateUsageMode();
40  property var overrideDeviceName: screens.count > 1 ? "desktop" : false
41 
42  DeviceConfiguration {
43  id: _deviceConfiguration
44 
45  // Override for convergence to set scale etc for second monitor
46  overrideName: root.overrideDeviceName
47  }
48 
49  Item {
50  id: d
51 
52  property Orientations orientations: Orientations {
53  id: orientations
54  // NB: native and primary orientations here don't map exactly to their QScreen counterparts
55  native_: root.width > root.height ? Qt.LandscapeOrientation : Qt.PortraitOrientation
56 
57  primary: deviceConfiguration.primaryOrientation == deviceConfiguration.useNativeOrientation
58  ? native_ : deviceConfiguration.primaryOrientation
59 
60  landscape: deviceConfiguration.landscapeOrientation
61  invertedLandscape: deviceConfiguration.invertedLandscapeOrientation
62  portrait: deviceConfiguration.portraitOrientation
63  invertedPortrait: deviceConfiguration.invertedPortraitOrientation
64  }
65  }
66 
67  GSettings {
68  id: lomiriSettings
69  schema.id: "com.lomiri.Shell"
70  }
71 
72  GSettings {
73  id: oskSettings
74  objectName: "oskSettings"
75  schema.id: "com.canonical.keyboard.maliit"
76  }
77 
78  property int physicalOrientation: Screen.orientation
79  property bool orientationLocked: OrientationLock.enabled
80  property var orientationLock: OrientationLock
81 
82  InputDeviceModel {
83  id: miceModel
84  deviceFilter: InputInfo.Mouse
85  property int oldCount: 0
86  }
87 
88  InputDeviceModel {
89  id: touchPadModel
90  deviceFilter: InputInfo.TouchPad
91  property int oldCount: 0
92  }
93 
94  InputDeviceModel {
95  id: keyboardsModel
96  deviceFilter: InputInfo.Keyboard
97  onDeviceAdded: forceOSKEnabled = autopilotDevicePresent();
98  onDeviceRemoved: forceOSKEnabled = autopilotDevicePresent();
99  }
100 
101  InputDeviceModel {
102  id: touchScreensModel
103  deviceFilter: InputInfo.TouchScreen
104  }
105 
106  Binding {
107  target: QuickUtils
108  property: "keyboardAttached"
109  value: keyboardsModel.count > 0
110  }
111 
112  readonly property int pointerInputDevices: miceModel.count + touchPadModel.count
113  onPointerInputDevicesChanged: calculateUsageMode()
114 
115  function calculateUsageMode() {
116  if (lomiriSettings.usageMode === undefined)
117  return; // gsettings isn't loaded yet, we'll try again in Component.onCompleted
118 
119  console.log("Calculating new usage mode. Pointer devices:", pointerInputDevices, "current mode:", lomiriSettings.usageMode, "old device count", miceModel.oldCount + touchPadModel.oldCount, "root width:", root.width / units.gu(1), "height:", root.height / units.gu(1))
120  if (lomiriSettings.usageMode === "Windowed") {
121  if (Math.min(root.width, root.height) > units.gu(60)) {
122  if (pointerInputDevices === 0) {
123  // All pointer devices have been unplugged. Move to staged.
124  lomiriSettings.usageMode = "Staged";
125  }
126  } else {
127  // The display is not large enough, use staged.
128  lomiriSettings.usageMode = "Staged";
129  }
130  } else {
131  if (Math.min(root.width, root.height) > units.gu(60)) {
132  if (pointerInputDevices > 0 && pointerInputDevices > miceModel.oldCount + touchPadModel.oldCount) {
133  lomiriSettings.usageMode = "Windowed";
134  }
135  } else {
136  // Make sure we initialize to something sane
137  lomiriSettings.usageMode = "Staged";
138  }
139  }
140  miceModel.oldCount = miceModel.count;
141  touchPadModel.oldCount = touchPadModel.count;
142  }
143 
144  /* FIXME: This exposes the NameRole as a work arround for lp:1542224.
145  * When QInputInfo exposes NameRole to QML, this should be removed.
146  */
147  property bool forceOSKEnabled: false
148  property var autopilotEmulatedDeviceNames: ["py-evdev-uinput"]
149  LomiriSortFilterProxyModel {
150  id: autopilotDevices
151  model: keyboardsModel
152  }
153 
154  function autopilotDevicePresent() {
155  for(var i = 0; i < autopilotDevices.count; i++) {
156  var device = autopilotDevices.get(i);
157  if (autopilotEmulatedDeviceNames.indexOf(device.name) != -1) {
158  console.warn("Forcing the OSK to be enabled as there is an autopilot eumlated device present.")
159  return true;
160  }
161  }
162  return false;
163  }
164 
165  property int orientation
166  onPhysicalOrientationChanged: {
167  if (!orientationLocked) {
168  orientation = physicalOrientation;
169  }
170  }
171  onOrientationLockedChanged: {
172  if (orientationLocked) {
173  orientationLock.savedOrientation = physicalOrientation;
174  } else {
175  orientation = physicalOrientation;
176  }
177  }
178  Component.onCompleted: {
179  if (orientationLocked) {
180  orientation = orientationLock.savedOrientation;
181  }
182 
183  calculateUsageMode();
184 
185  // We need to manually update this on startup as the binding
186  // below doesn't seem to have any effect at that stage
187  oskSettings.disableHeight = !shell.oskEnabled || shell.usageScenario == "desktop"
188  }
189 
190  // we must rotate to a supported orientation regardless of shell's preference
191  property bool orientationChangesEnabled:
192  (shell.orientation & supportedOrientations) === 0 ? true
193  : shell.orientationChangesEnabled
194 
195  Binding {
196  target: oskSettings
197  property: "disableHeight"
198  value: !shell.oskEnabled || shell.usageScenario == "desktop"
199  }
200 
201  Binding {
202  target: lomiriSettings
203  property: "oskSwitchVisible"
204  value: shell.hasKeyboard
205  }
206 
207  readonly property int supportedOrientations: shell.supportedOrientations
208  & (deviceConfiguration.supportedOrientations == deviceConfiguration.useNativeOrientation
209  ? orientations.native_
210  : deviceConfiguration.supportedOrientations)
211 
212  property int acceptedOrientationAngle: {
213  if (orientation & supportedOrientations) {
214  return Screen.angleBetween(orientations.native_, orientation);
215  } else if (shell.orientation & supportedOrientations) {
216  // stay where we are
217  return shell.orientationAngle;
218  } else if (angleToOrientation(shell.mainAppWindowOrientationAngle) & supportedOrientations) {
219  return shell.mainAppWindowOrientationAngle;
220  } else {
221  // rotate to some supported orientation as we can't stay where we currently are
222  // TODO: Choose the closest to the current one
223  if (supportedOrientations & Qt.PortraitOrientation) {
224  return Screen.angleBetween(orientations.native_, Qt.PortraitOrientation);
225  } else if (supportedOrientations & Qt.LandscapeOrientation) {
226  return Screen.angleBetween(orientations.native_, Qt.LandscapeOrientation);
227  } else if (supportedOrientations & Qt.InvertedPortraitOrientation) {
228  return Screen.angleBetween(orientations.native_, Qt.InvertedPortraitOrientation);
229  } else if (supportedOrientations & Qt.InvertedLandscapeOrientation) {
230  return Screen.angleBetween(orientations.native_, Qt.InvertedLandscapeOrientation);
231  } else {
232  // if all fails, fallback to primary orientation
233  return Screen.angleBetween(orientations.native_, orientations.primary);
234  }
235  }
236  }
237 
238  function angleToOrientation(angle) {
239  switch (angle) {
240  case 0:
241  return orientations.native_;
242  case 90:
243  return orientations.native_ === Qt.PortraitOrientation ? Qt.InvertedLandscapeOrientation
244  : Qt.PortraitOrientation;
245  case 180:
246  return orientations.native_ === Qt.PortraitOrientation ? Qt.InvertedPortraitOrientation
247  : Qt.InvertedLandscapeOrientation;
248  case 270:
249  return orientations.native_ === Qt.PortraitOrientation ? Qt.LandscapeOrientation
250  : Qt.InvertedPortraitOrientation;
251  default:
252  console.warn("angleToOrientation: Invalid orientation angle: " + angle);
253  return orientations.primary;
254  }
255  }
256 
257  RotationStates {
258  id: rotationStates
259  objectName: "rotationStates"
260  orientedShell: root
261  shell: shell
262  shellCover: shellCover
263  shellSnapshot: shellSnapshot
264  }
265 
266  Shell {
267  id: shell
268  objectName: "shell"
269  width: root.width
270  height: root.height
271  orientation: root.angleToOrientation(orientationAngle)
272  orientations: root.orientations
273  nativeWidth: root.width
274  nativeHeight: root.height
275  mode: applicationArguments.mode
276  hasMouse: pointerInputDevices > 0
277  hasKeyboard: keyboardsModel.count > 0
278  hasTouchscreen: touchScreensModel.count > 0
279  supportsMultiColorLed: deviceConfiguration.supportsMultiColorLed
280  lightIndicators: root.lightIndicators
281 
282  // Since we dont have proper multiscreen support yet
283  // hardcode screen count to only show osk on this screen
284  // when it's the only one connected.
285  // FIXME once multiscreen has landed
286  oskEnabled: (!hasKeyboard && Screens.count === 1) ||
287  lomiriSettings.alwaysShowOsk || forceOSKEnabled
288 
289  usageScenario: {
290  if (lomiriSettings.usageMode === "Windowed") {
291  return "desktop";
292  } else {
293  if (deviceConfiguration.category === "phone") {
294  return "phone";
295  } else {
296  return "tablet";
297  }
298  }
299  }
300 
301  property real transformRotationAngle
302  property real transformOriginX
303  property real transformOriginY
304 
305  transform: Rotation {
306  origin.x: shell.transformOriginX; origin.y: shell.transformOriginY; axis { x: 0; y: 0; z: 1 }
307  angle: shell.transformRotationAngle
308  }
309  }
310 
311  Rectangle {
312  id: shellCover
313  color: "black"
314  anchors.fill: parent
315  visible: false
316  }
317 
318  ItemSnapshot {
319  id: shellSnapshot
320  target: shell
321  visible: false
322  width: root.width
323  height: root.height
324 
325  property real transformRotationAngle
326  property real transformOriginX
327  property real transformOriginY
328 
329  transform: Rotation {
330  origin.x: shellSnapshot.transformOriginX; origin.y: shellSnapshot.transformOriginY;
331  axis { x: 0; y: 0; z: 1 }
332  angle: shellSnapshot.transformRotationAngle
333  }
334  }
335 }