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