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