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