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 Connections {
42 target: screen
43 function onFormFactorChanged() { calculateUsageMode(); }
44 }
45
46 onWidthChanged: calculateUsageMode();
47 property var overrideDeviceName: Screens.count > 1 ? "desktop" : false
48
49 DeviceConfiguration {
50 id: _deviceConfiguration
51
52 // Override for convergence to set scale etc for second monitor
53 overrideName: root.overrideDeviceName
54 }
55
56 Item {
57 id: d
58
59 property Orientations orientations: Orientations {
60 id: orientations
61 // NB: native and primary orientations here don't map exactly to their QScreen counterparts
62 native_: root.width > root.height ? Qt.LandscapeOrientation : Qt.PortraitOrientation
63
64 primary: deviceConfiguration.primaryOrientation == deviceConfiguration.useNativeOrientation
65 ? native_ : deviceConfiguration.primaryOrientation
66
67 landscape: deviceConfiguration.landscapeOrientation
68 invertedLandscape: deviceConfiguration.invertedLandscapeOrientation
69 portrait: deviceConfiguration.portraitOrientation
70 invertedPortrait: deviceConfiguration.invertedPortraitOrientation
71 }
72 }
73
74 GSettings {
75 id: lomiriSettings
76 schema.id: "com.lomiri.Shell"
77 }
78
79 GSettings {
80 id: oskSettings
81 objectName: "oskSettings"
82 schema.id: "com.lomiri.keyboard.maliit"
83 }
84
85 property int physicalOrientation: QtQuickWindow.Screen.orientation
86 property bool orientationLocked: OrientationLock.enabled
87 property var orientationLock: OrientationLock
88
89 InputDeviceModel {
90 id: miceModel
91 deviceFilter: InputInfo.Mouse
92 property int oldCount: 0
93 }
94
95 InputDeviceModel {
96 id: touchPadModel
97 deviceFilter: InputInfo.TouchPad
98 property int oldCount: 0
99 }
100
101 InputDeviceModel {
102 id: keyboardsModel
103 deviceFilter: InputInfo.Keyboard
104 onDeviceAdded: forceOSKEnabled = autopilotDevicePresent();
105 onDeviceRemoved: forceOSKEnabled = autopilotDevicePresent();
106 }
107
108 InputDeviceModel {
109 id: touchScreensModel
110 deviceFilter: InputInfo.TouchScreen
111 }
112
113 Binding {
114 target: QuickUtils
115 property: "keyboardAttached"
116 value: keyboardsModel.count > 0
117 restoreMode: Binding.RestoreBinding
118 }
119
120 readonly property int pointerInputDevices: miceModel.count + touchPadModel.count
121 onPointerInputDevicesChanged: calculateUsageMode()
122
123 function calculateUsageMode() {
124 if (lomiriSettings.usageMode === undefined)
125 return; // gsettings isn't loaded yet, we'll try again in Component.onCompleted
126
127 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)
128 if (lomiriSettings.usageMode === "Windowed") {
129 if (Math.min(root.width, root.height) > units.gu(60)) {
130 if (pointerInputDevices === 0) {
131 // All pointer devices have been unplugged. Move to staged.
132 lomiriSettings.usageMode = "Staged";
133 }
134 } else {
135 // The display is not large enough, use staged.
136 lomiriSettings.usageMode = "Staged";
137 }
138 } else {
139 if (Math.min(root.width, root.height) > units.gu(60)) {
140 if (pointerInputDevices > 0 && pointerInputDevices > miceModel.oldCount + touchPadModel.oldCount) {
141 lomiriSettings.usageMode = "Windowed";
142 }
143 } else {
144 // Make sure we initialize to something sane
145 lomiriSettings.usageMode = "Staged";
146 }
147 }
148 miceModel.oldCount = miceModel.count;
149 touchPadModel.oldCount = touchPadModel.count;
150 }
151
152 /* FIXME: This exposes the NameRole as a work arround for lp:1542224.
153 * When QInputInfo exposes NameRole to QML, this should be removed.
154 */
155 property bool forceOSKEnabled: false
156 property var autopilotEmulatedDeviceNames: ["py-evdev-uinput"]
157 LomiriSortFilterProxyModel {
158 id: autopilotDevices
159 model: keyboardsModel
160 }
161
162 function autopilotDevicePresent() {
163 for(var i = 0; i < autopilotDevices.count; i++) {
164 var device = autopilotDevices.get(i);
165 if (autopilotEmulatedDeviceNames.indexOf(device.name) != -1) {
166 console.warn("Forcing the OSK to be enabled as there is an autopilot eumlated device present.")
167 return true;
168 }
169 }
170 return false;
171 }
172
173 property int orientation
174 onPhysicalOrientationChanged: {
175 if (!orientationLocked) {
176 orientation = physicalOrientation;
177 } else {
178 if (orientation !== physicalOrientation && !shell.showingGreeter) {
179 rotateButton.show()
180 } else {
181 rotateButton.hide()
182 }
183 }
184 }
185 onOrientationLockedChanged: {
186 if (orientationLocked) {
187 orientationLock.savedOrientation = physicalOrientation;
188 } else {
189 orientation = physicalOrientation;
190 }
191 }
192 Component.onCompleted: {
193 if (orientationLocked) {
194 orientation = orientationLock.savedOrientation;
195 }
196
197 calculateUsageMode();
198
199 // We need to manually update this on startup as the binding
200 // below doesn't seem to have any effect at that stage
201 oskSettings.disableHeight = !shell.oskEnabled || shell.usageScenario == "desktop"
202 }
203
204 Component.onDestruction: {
205 const from_workspaces = root.screen.workspaces;
206 const from_workspaces_size = from_workspaces.count;
207 for (var i = 0; i < from_workspaces_size; i++) {
208 const from = from_workspaces.get(i);
209 WorkspaceManager.destroyWorkspace(from);
210 }
211 }
212
213 // we must rotate to a supported orientation regardless of shell's preference
214 property bool orientationChangesEnabled:
215 (shell.orientation & supportedOrientations) === 0 ? true
216 : shell.orientationChangesEnabled
217
218 Binding {
219 target: oskSettings
220 restoreMode: Binding.RestoreBinding
221 property: "disableHeight"
222 value: !shell.oskEnabled || shell.usageScenario == "desktop"
223 }
224
225 Binding {
226 target: lomiriSettings
227 restoreMode: Binding.RestoreBinding
228 property: "oskSwitchVisible"
229 value: shell.hasKeyboard
230 }
231
232 readonly property int supportedOrientations: shell.supportedOrientations
233 & (deviceConfiguration.supportedOrientations == deviceConfiguration.useNativeOrientation
234 ? orientations.native_
235 : deviceConfiguration.supportedOrientations)
236
237 // During desktop mode switches back to phone mode Qt seems to swallow
238 // supported orientations by itself, not emitting them. Cause them to be emitted
239 // using the attached property here.
240 QtQuickWindow.Screen.orientationUpdateMask: supportedOrientations
241
242 property int acceptedOrientationAngle: {
243 if (orientation & supportedOrientations) {
244 return QtQuickWindow.Screen.angleBetween(orientations.native_, orientation);
245 } else if (shell.orientation & supportedOrientations) {
246 // stay where we are
247 return shell.orientationAngle;
248 } else if (angleToOrientation(shell.mainAppWindowOrientationAngle) & supportedOrientations) {
249 return shell.mainAppWindowOrientationAngle;
250 } else {
251 // rotate to some supported orientation as we can't stay where we currently are
252 // TODO: Choose the closest to the current one
253 if (supportedOrientations & Qt.PortraitOrientation) {
254 return QtQuickWindow.Screen.angleBetween(orientations.native_, Qt.PortraitOrientation);
255 } else if (supportedOrientations & Qt.LandscapeOrientation) {
256 return QtQuickWindow.Screen.angleBetween(orientations.native_, Qt.LandscapeOrientation);
257 } else if (supportedOrientations & Qt.InvertedPortraitOrientation) {
258 return QtQuickWindow.Screen.angleBetween(orientations.native_, Qt.InvertedPortraitOrientation);
259 } else if (supportedOrientations & Qt.InvertedLandscapeOrientation) {
260 return QtQuickWindow.Screen.angleBetween(orientations.native_, Qt.InvertedLandscapeOrientation);
261 } else {
262 // if all fails, fallback to primary orientation
263 return QtQuickWindow.Screen.angleBetween(orientations.native_, orientations.primary);
264 }
265 }
266 }
267
268 function angleToOrientation(angle) {
269 switch (angle) {
270 case 0:
271 return orientations.native_;
272 case 90:
273 return orientations.native_ === Qt.PortraitOrientation ? Qt.InvertedLandscapeOrientation
274 : Qt.PortraitOrientation;
275 case 180:
276 return orientations.native_ === Qt.PortraitOrientation ? Qt.InvertedPortraitOrientation
277 : Qt.InvertedLandscapeOrientation;
278 case 270:
279 return orientations.native_ === Qt.PortraitOrientation ? Qt.LandscapeOrientation
280 : Qt.InvertedPortraitOrientation;
281 default:
282 console.warn("angleToOrientation: Invalid orientation angle: " + angle);
283 return orientations.primary;
284 }
285 }
286
287 RotationStates {
288 id: rotationStates
289 objectName: "rotationStates"
290 orientedShell: root
291 shell: shell
292 shellCover: shellCover
293 shellSnapshot: shellSnapshot
294 }
295
296 Shell {
297 id: shell
298 objectName: "shell"
299 width: root.width
300 height: root.height
301 orientation: root.angleToOrientation(orientationAngle)
302 orientations: root.orientations
303 nativeWidth: root.width
304 nativeHeight: root.height
305 mode: applicationArguments.mode
306 hasMouse: pointerInputDevices > 0
307 hasKeyboard: keyboardsModel.count > 0
308 hasTouchscreen: touchScreensModel.count > 0
309 supportsMultiColorLed: deviceConfiguration.supportsMultiColorLed
310 lightIndicators: root.lightIndicators
311 oskEnabled: (!hasKeyboard && Screens.count === 1) ||
312 lomiriSettings.alwaysShowOsk || forceOSKEnabled
313
314 // Multiscreen support: in addition to judging by the device type, go by the screen type.
315 // This allows very flexible usecases beyond the typical "connect a phone to a monitor".
316 // Status quo setups:
317 // - phone + external monitor: virtual touchpad on the device
318 // - tablet + external monitor: dual-screen desktop
319 // - desktop: Has all the bells and whistles of a fully fledged PC/laptop shell
320 usageScenario: {
321 if (lomiriSettings.usageMode === "Windowed") {
322 return "desktop";
323 } else if (deviceConfiguration.category === "phone") {
324 return "phone";
325 } else if (deviceConfiguration.category === "tablet") {
326 return "tablet";
327 } else {
328 if (screen.formFactor === Screen.Tablet) {
329 return "tablet";
330 } else if (shell.hasTouchscreen) {
331 return "tablet";
332 } else if (screen.formFactor === Screen.Phone) {
333 return "phone";
334 } else {
335 return "desktop";
336 }
337 }
338 }
339
340 property real transformRotationAngle
341 property real transformOriginX
342 property real transformOriginY
343
344 transform: Rotation {
345 origin.x: shell.transformOriginX; origin.y: shell.transformOriginY; axis { x: 0; y: 0; z: 1 }
346 angle: shell.transformRotationAngle
347 }
348 }
349
350 Rectangle {
351 id: rotateButton
352
353 readonly property real visibleOpacity: 0.8
354 readonly property bool rotateAvailable: root.orientationLocked && root.physicalOrientation !== root.orientation
355
356 anchors.margins: units.gu(3)
357 states: [
358 State {
359 when: !rotateButton.rotateAvailable
360 AnchorChanges {
361 target: rotateButton
362 anchors.right: parent.left
363 anchors.top: parent.bottom
364 }
365 }
366 , State {
367 when: rotateButton.rotateAvailable && root.physicalOrientation == Qt.InvertedLandscapeOrientation
368 AnchorChanges {
369 target: rotateButton
370 anchors.left: parent.left
371 anchors.bottom: parent.bottom
372 }
373 }
374 , State {
375 when: rotateButton.rotateAvailable && root.physicalOrientation == Qt.LandscapeOrientation
376 AnchorChanges {
377 target: rotateButton
378 anchors.right: parent.right
379 anchors.top: parent.top
380 }
381 PropertyChanges {
382 target: rotateButton
383 anchors.topMargin: shell.shellMargin
384 }
385 }
386 , State {
387 when: rotateButton.rotateAvailable && root.physicalOrientation == Qt.PortraitOrientation
388 AnchorChanges {
389 target: rotateButton
390 anchors.right: parent.right
391 anchors.bottom: parent.bottom
392 }
393 }
394 , State {
395 when: rotateButton.rotateAvailable && root.physicalOrientation == Qt.InvertedPortraitOrientation
396 AnchorChanges {
397 target: rotateButton
398 anchors.left: parent.left
399 anchors.top: parent.top
400 }
401 PropertyChanges {
402 target: rotateButton
403 anchors.topMargin: shell.shellMargin
404 }
405 }
406 ]
407 height: units.gu(4)
408 width: height
409 radius: width / 2
410 visible: opacity > 0
411 opacity: 0
412 color: theme.palette.normal.background
413 border {
414 width: units.dp(1)
415 color: theme.palette.normal.backgroundText
416 }
417
418 function show() {
419 if (!visible) {
420 showDelay.restart()
421 }
422 }
423
424 function hide() {
425 hideAnimation.restart()
426 showDelay.stop()
427 }
428
429 Icon {
430 id: icon
431
432 implicitWidth: units.gu(3)
433 implicitHeight: implicitWidth
434 anchors.centerIn: parent
435 name: "view-rotate"
436 color: theme.palette.normal.backgroundText
437 }
438
439 MouseArea {
440 anchors.fill: parent
441 onClicked: {
442 rotateButton.hide()
443 orientationLock.savedOrientation = root.orientation
444 root.orientation = root.physicalOrientation
445 }
446 }
447
448 LomiriNumberAnimation {
449 id: showAnimation
450
451 running: false
452 property: "opacity"
453 target: rotateButton
454 alwaysRunToEnd: true
455 to: rotateButton.visibleOpacity
456 duration: LomiriAnimation.SlowDuration
457 }
458
459 LomiriNumberAnimation {
460 id: hideAnimation
461
462 running: false
463 property: "opacity"
464 target: rotateButton
465 alwaysRunToEnd: true
466 to: 0
467 duration: LomiriAnimation.FastDuration
468 }
469
470 SequentialAnimation {
471 running: rotateButton.visible
472 loops: 3
473 RotationAnimation {
474 target: rotateButton
475 duration: LomiriAnimation.SnapDuration
476 to: 0
477 direction: RotationAnimation.Shortest
478 }
479 NumberAnimation { target: icon; duration: LomiriAnimation.SnapDuration; property: "opacity"; to: 1 }
480 PauseAnimation { duration: LomiriAnimation.SlowDuration }
481 RotationAnimation {
482 target: rotateButton
483 duration: LomiriAnimation.SlowDuration
484 to: root.orientationLocked ? QtQuickWindow.Screen.angleBetween(root.orientation, root.physicalOrientation) : 0
485 direction: RotationAnimation.Shortest
486 }
487 PauseAnimation { duration: LomiriAnimation.SlowDuration }
488 NumberAnimation { target: icon; duration: LomiriAnimation.SnapDuration; property: "opacity"; to: 0 }
489
490 onFinished: rotateButton.hide()
491 }
492
493 Timer {
494 id: showDelay
495
496 running: false
497 interval: 1000
498 onTriggered: {
499 showAnimation.restart()
500 }
501 }
502
503 Timer {
504 id: hideDelay
505
506 running: false
507 interval: 3000
508 onTriggered: rotateButton.hide()
509 }
510 }
511
512 Rectangle {
513 id: shellCover
514 color: "black"
515 anchors.fill: parent
516 visible: false
517 }
518
519 ItemSnapshot {
520 id: shellSnapshot
521 target: shell
522 visible: false
523 width: root.width
524 height: root.height
525
526 property real transformRotationAngle
527 property real transformOriginX
528 property real transformOriginY
529
530 transform: Rotation {
531 origin.x: shellSnapshot.transformOriginX; origin.y: shellSnapshot.transformOriginY;
532 axis { x: 0; y: 0; z: 1 }
533 angle: shellSnapshot.transformRotationAngle
534 }
535 }
536}