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.12
18import QtQuick.Window 2.2 as QtQuickWindow
19import Lomiri.InputInfo 0.1
20import Lomiri.Session 0.1
21import WindowManager 1.0
22import Utils 0.1
23import GSettings 1.0
24import "Components"
25import "Rotation"
26// Workaround https://bugs.launchpad.net/lomiri/+source/lomiri/+bug/1473471
27import Lomiri.Components 1.3
28
29Item {
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 } else {
176 if (orientation !== physicalOrientation && !shell.showingGreeter) {
177 rotateButton.show()
178 } else {
179 rotateButton.hide()
180 }
181 }
182 }
183 onOrientationLockedChanged: {
184 if (orientationLocked) {
185 orientationLock.savedOrientation = physicalOrientation;
186 } else {
187 orientation = physicalOrientation;
188 }
189 }
190 Component.onCompleted: {
191 if (orientationLocked) {
192 orientation = orientationLock.savedOrientation;
193 }
194
195 calculateUsageMode();
196
197 // We need to manually update this on startup as the binding
198 // below doesn't seem to have any effect at that stage
199 oskSettings.disableHeight = !shell.oskEnabled || shell.usageScenario == "desktop"
200 }
201
202 Component.onDestruction: {
203 const from_workspaces = root.screen.workspaces;
204 const from_workspaces_size = from_workspaces.count;
205 for (var i = 0; i < from_workspaces_size; i++) {
206 const from = from_workspaces.get(i);
207 WorkspaceManager.destroyWorkspace(from);
208 }
209 }
210
211 // we must rotate to a supported orientation regardless of shell's preference
212 property bool orientationChangesEnabled:
213 (shell.orientation & supportedOrientations) === 0 ? true
214 : shell.orientationChangesEnabled
215
216 Binding {
217 target: oskSettings
218 property: "disableHeight"
219 value: !shell.oskEnabled || shell.usageScenario == "desktop"
220 }
221
222 Binding {
223 target: lomiriSettings
224 property: "oskSwitchVisible"
225 value: shell.hasKeyboard
226 }
227
228 readonly property int supportedOrientations: shell.supportedOrientations
229 & (deviceConfiguration.supportedOrientations == deviceConfiguration.useNativeOrientation
230 ? orientations.native_
231 : deviceConfiguration.supportedOrientations)
232
233 // During desktop mode switches back to phone mode Qt seems to swallow
234 // supported orientations by itself, not emitting them. Cause them to be emitted
235 // using the attached property here.
236 QtQuickWindow.Screen.orientationUpdateMask: supportedOrientations
237
238 property int acceptedOrientationAngle: {
239 if (orientation & supportedOrientations) {
240 return QtQuickWindow.Screen.angleBetween(orientations.native_, orientation);
241 } else if (shell.orientation & supportedOrientations) {
242 // stay where we are
243 return shell.orientationAngle;
244 } else if (angleToOrientation(shell.mainAppWindowOrientationAngle) & supportedOrientations) {
245 return shell.mainAppWindowOrientationAngle;
246 } else {
247 // rotate to some supported orientation as we can't stay where we currently are
248 // TODO: Choose the closest to the current one
249 if (supportedOrientations & Qt.PortraitOrientation) {
250 return QtQuickWindow.Screen.angleBetween(orientations.native_, Qt.PortraitOrientation);
251 } else if (supportedOrientations & Qt.LandscapeOrientation) {
252 return QtQuickWindow.Screen.angleBetween(orientations.native_, Qt.LandscapeOrientation);
253 } else if (supportedOrientations & Qt.InvertedPortraitOrientation) {
254 return QtQuickWindow.Screen.angleBetween(orientations.native_, Qt.InvertedPortraitOrientation);
255 } else if (supportedOrientations & Qt.InvertedLandscapeOrientation) {
256 return QtQuickWindow.Screen.angleBetween(orientations.native_, Qt.InvertedLandscapeOrientation);
257 } else {
258 // if all fails, fallback to primary orientation
259 return QtQuickWindow.Screen.angleBetween(orientations.native_, orientations.primary);
260 }
261 }
262 }
263
264 function angleToOrientation(angle) {
265 switch (angle) {
266 case 0:
267 return orientations.native_;
268 case 90:
269 return orientations.native_ === Qt.PortraitOrientation ? Qt.InvertedLandscapeOrientation
270 : Qt.PortraitOrientation;
271 case 180:
272 return orientations.native_ === Qt.PortraitOrientation ? Qt.InvertedPortraitOrientation
273 : Qt.InvertedLandscapeOrientation;
274 case 270:
275 return orientations.native_ === Qt.PortraitOrientation ? Qt.LandscapeOrientation
276 : Qt.InvertedPortraitOrientation;
277 default:
278 console.warn("angleToOrientation: Invalid orientation angle: " + angle);
279 return orientations.primary;
280 }
281 }
282
283 RotationStates {
284 id: rotationStates
285 objectName: "rotationStates"
286 orientedShell: root
287 shell: shell
288 shellCover: shellCover
289 shellSnapshot: shellSnapshot
290 }
291
292 Shell {
293 id: shell
294 objectName: "shell"
295 width: root.width
296 height: root.height
297 orientation: root.angleToOrientation(orientationAngle)
298 orientations: root.orientations
299 nativeWidth: root.width
300 nativeHeight: root.height
301 mode: applicationArguments.mode
302 hasMouse: pointerInputDevices > 0
303 hasKeyboard: keyboardsModel.count > 0
304 hasTouchscreen: touchScreensModel.count > 0
305 supportsMultiColorLed: deviceConfiguration.supportsMultiColorLed
306 lightIndicators: root.lightIndicators
307 oskEnabled: (!hasKeyboard && Screens.count === 1) ||
308 lomiriSettings.alwaysShowOsk || forceOSKEnabled
309
310 // Multiscreen support: in addition to judging by the device type, go by the screen type.
311 // This allows very flexible usecases beyond the typical "connect a phone to a monitor".
312 // Status quo setups:
313 // - phone + external monitor: virtual touchpad on the device
314 // - tablet + external monitor: dual-screen desktop
315 // - desktop: Has all the bells and whistles of a fully fledged PC/laptop shell
316 usageScenario: {
317 if (lomiriSettings.usageMode === "Windowed") {
318 return "desktop";
319 } else if (deviceConfiguration.category === "phone") {
320 return "phone";
321 } else if (deviceConfiguration.category === "tablet") {
322 return "tablet";
323 } else {
324 if (screen.formFactor === Screen.Tablet) {
325 return "tablet";
326 } else if (shell.hasTouchscreen) {
327 return "tablet";
328 } else if (screen.formFactor === Screen.Phone) {
329 return "phone";
330 } else {
331 return "desktop";
332 }
333 }
334 }
335
336 property real transformRotationAngle
337 property real transformOriginX
338 property real transformOriginY
339
340 transform: Rotation {
341 origin.x: shell.transformOriginX; origin.y: shell.transformOriginY; axis { x: 0; y: 0; z: 1 }
342 angle: shell.transformRotationAngle
343 }
344 }
345
346 Rectangle {
347 id: rotateButton
348
349 readonly property real visibleOpacity: 0.8
350 readonly property bool rotateAvailable: root.orientationLocked && root.physicalOrientation !== root.orientation
351
352 anchors.margins: units.gu(3)
353 states: [
354 State {
355 when: !rotateButton.rotateAvailable
356 AnchorChanges {
357 target: rotateButton
358 anchors.right: parent.left
359 anchors.top: parent.bottom
360 }
361 }
362 , State {
363 when: rotateButton.rotateAvailable && root.physicalOrientation == Qt.InvertedLandscapeOrientation
364 AnchorChanges {
365 target: rotateButton
366 anchors.left: parent.left
367 anchors.bottom: parent.bottom
368 }
369 }
370 , State {
371 when: rotateButton.rotateAvailable && root.physicalOrientation == Qt.LandscapeOrientation
372 AnchorChanges {
373 target: rotateButton
374 anchors.right: parent.right
375 anchors.top: parent.top
376 }
377 PropertyChanges {
378 target: rotateButton
379 anchors.topMargin: shell.shellMargin
380 }
381 }
382 , State {
383 when: rotateButton.rotateAvailable && root.physicalOrientation == Qt.PortraitOrientation
384 AnchorChanges {
385 target: rotateButton
386 anchors.right: parent.right
387 anchors.bottom: parent.bottom
388 }
389 }
390 , State {
391 when: rotateButton.rotateAvailable && root.physicalOrientation == Qt.InvertedPortraitOrientation
392 AnchorChanges {
393 target: rotateButton
394 anchors.left: parent.left
395 anchors.top: parent.top
396 }
397 PropertyChanges {
398 target: rotateButton
399 anchors.topMargin: shell.shellMargin
400 }
401 }
402 ]
403 height: units.gu(4)
404 width: height
405 radius: width / 2
406 visible: opacity > 0
407 opacity: 0
408 color: theme.palette.normal.background
409 border {
410 width: units.dp(1)
411 color: theme.palette.normal.backgroundText
412 }
413
414 function show() {
415 if (!visible) {
416 showDelay.restart()
417 }
418 }
419
420 function hide() {
421 hideAnimation.restart()
422 showDelay.stop()
423 }
424
425 Icon {
426 id: icon
427
428 implicitWidth: units.gu(3)
429 implicitHeight: implicitWidth
430 anchors.centerIn: parent
431 name: "view-rotate"
432 color: theme.palette.normal.backgroundText
433 }
434
435 MouseArea {
436 anchors.fill: parent
437 onClicked: {
438 rotateButton.hide()
439 orientationLock.savedOrientation = root.orientation
440 root.orientation = root.physicalOrientation
441 }
442 }
443
444 LomiriNumberAnimation {
445 id: showAnimation
446
447 running: false
448 property: "opacity"
449 target: rotateButton
450 alwaysRunToEnd: true
451 to: rotateButton.visibleOpacity
452 duration: LomiriAnimation.SlowDuration
453 }
454
455 LomiriNumberAnimation {
456 id: hideAnimation
457
458 running: false
459 property: "opacity"
460 target: rotateButton
461 alwaysRunToEnd: true
462 to: 0
463 duration: LomiriAnimation.FastDuration
464 }
465
466 SequentialAnimation {
467 running: rotateButton.visible
468 loops: 3
469 RotationAnimation {
470 target: rotateButton
471 duration: LomiriAnimation.SnapDuration
472 to: 0
473 direction: RotationAnimation.Shortest
474 }
475 NumberAnimation { target: icon; duration: LomiriAnimation.SnapDuration; property: "opacity"; to: 1 }
476 PauseAnimation { duration: LomiriAnimation.SlowDuration }
477 RotationAnimation {
478 target: rotateButton
479 duration: LomiriAnimation.SlowDuration
480 to: root.orientationLocked ? QtQuickWindow.Screen.angleBetween(root.orientation, root.physicalOrientation) : 0
481 direction: RotationAnimation.Shortest
482 }
483 PauseAnimation { duration: LomiriAnimation.SlowDuration }
484 NumberAnimation { target: icon; duration: LomiriAnimation.SnapDuration; property: "opacity"; to: 0 }
485
486 onFinished: rotateButton.hide()
487 }
488
489 Timer {
490 id: showDelay
491
492 running: false
493 interval: 1000
494 onTriggered: {
495 showAnimation.restart()
496 }
497 }
498
499 Timer {
500 id: hideDelay
501
502 running: false
503 interval: 3000
504 onTriggered: rotateButton.hide()
505 }
506 }
507
508 Rectangle {
509 id: shellCover
510 color: "black"
511 anchors.fill: parent
512 visible: false
513 }
514
515 ItemSnapshot {
516 id: shellSnapshot
517 target: shell
518 visible: false
519 width: root.width
520 height: root.height
521
522 property real transformRotationAngle
523 property real transformOriginX
524 property real transformOriginY
525
526 transform: Rotation {
527 origin.x: shellSnapshot.transformOriginX; origin.y: shellSnapshot.transformOriginY;
528 axis { x: 0; y: 0; z: 1 }
529 angle: shellSnapshot.transformRotationAngle
530 }
531 }
532}