on 7a1fa19 help sign in
unity8/qml/Shell.qml
880 lines 28 kB history raw
1/*
2 * Copyright (C) 2013 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 */
17import QtQuick 2.0
18import AccountsService 0.1
19import GSettings 1.0
20import Unity.Application 0.1
21import Ubuntu.Components 0.1
22import Ubuntu.Components.Popups 0.1
23import Ubuntu.Gestures 0.1
24import Unity.Launcher 0.1
25import LightDM 0.1 as LightDM
26import Powerd 0.1
27import SessionBroadcast 0.1
28import "Dash"
29import "Greeter"
30import "Launcher"
31import "Panel"
32import "Components"
33import "Notifications"
34import Unity.Notifications 1.0 as NotificationBackend
35import Unity.Session 0.1
37FocusScope {
38 id: shell
40 // this is only here to select the width / height of the window if not running fullscreen
41 property bool tablet: false
42 width: tablet ? units.gu(160) : applicationArguments.hasGeometry() ? applicationArguments.width() : units.gu(40)
43 height: tablet ? units.gu(100) : applicationArguments.hasGeometry() ? applicationArguments.height() : units.gu(71)
45 property real edgeSize: units.gu(2)
46 property url defaultBackground: Qt.resolvedUrl(shell.width >= units.gu(60) ? "graphics/tablet_background.jpg" : "graphics/phone_background.jpg")
47 property url background
48 readonly property real panelHeight: panel.panelHeight
50 property bool dashShown: dash.shown
52 property bool sideStageEnabled: shell.width >= units.gu(100)
53 readonly property string focusedApplicationId: ApplicationManager.focusedApplicationId
55 function activateApplication(appId) {
56 if (ApplicationManager.findApplication(appId)) {
57 ApplicationManager.requestFocusApplication(appId);
58 stages.show(true);
59 if (stages.locked && ApplicationManager.focusedApplicationId == appId) {
60 applicationsDisplayLoader.item.select(appId);
61 }
62 } else {
63 var execFlags = shell.sideStageEnabled ? ApplicationManager.NoFlag : ApplicationManager.ForceMainStage;
64 ApplicationManager.startApplication(appId, execFlags);
65 stages.show(false);
66 }
67 }
69 Binding {
70 target: LauncherModel
71 property: "applicationManager"
72 value: ApplicationManager
73 }
75 Component.onCompleted: {
76 Theme.name = "Ubuntu.Components.Themes.SuruGradient"
77 }
79 GSettings {
80 id: backgroundSettings
81 schema.id: "org.gnome.desktop.background"
82 }
83 property url gSettingsPicture: backgroundSettings.pictureUri != undefined && backgroundSettings.pictureUri.length > 0 ? backgroundSettings.pictureUri : shell.defaultBackground
84 onGSettingsPictureChanged: {
85 shell.background = gSettingsPicture
86 }
88 VolumeControl {
89 id: volumeControl
90 }
92 Keys.onVolumeUpPressed: volumeControl.volumeUp()
93 Keys.onVolumeDownPressed: volumeControl.volumeDown()
95 Item {
96 id: underlayClipper
97 anchors.fill: parent
98 anchors.rightMargin: stages.overlayWidth
99 clip: stages.overlayMode && !stages.painting
101 InputFilterArea {
102 anchors.fill: parent
103 blockInput: parent.clip
104 }
106 Item {
107 id: underlay
108 objectName: "underlay"
109 anchors.fill: parent
110 anchors.rightMargin: -parent.anchors.rightMargin
112 // Whether the underlay is fully covered by opaque UI elements.
113 property bool fullyCovered: panel.indicators.fullyOpened && shell.width <= panel.indicators.width
115 // Whether the user should see the topmost application surface (if there's one at all).
116 readonly property bool applicationSurfaceShouldBeSeen: stages.shown && !stages.painting && !stages.overlayMode
118 // NB! Application surfaces are stacked behind the shell one. So they can only be seen by the user
119 // through the translucent parts of the shell surface.
120 visible: !fullyCovered && !applicationSurfaceShouldBeSeen
122 Rectangle {
123 anchors.fill: parent
124 color: "black"
125 opacity: dash.disappearingAnimationProgress
126 }
128 Image {
129 anchors.fill: dash
130 source: shell.width > shell.height ? "Dash/graphics/paper_landscape.png" : "Dash/graphics/paper_portrait.png"
131 fillMode: Image.PreserveAspectCrop
132 horizontalAlignment: Image.AlignRight
133 verticalAlignment: Image.AlignTop
134 }
136 Dash {
137 id: dash
138 objectName: "dash"
140 available: !greeter.shown && !lockscreen.shown
141 hides: [stages, launcher, panel.indicators]
142 shown: disappearingAnimationProgress !== 1.0 && greeterWrapper.showProgress !== 1.0
143 enabled: disappearingAnimationProgress === 0.0 && greeterWrapper.showProgress === 0.0 && edgeDemo.dashEnabled
145 anchors {
146 fill: parent
147 topMargin: panel.panelHeight
148 }
150 contentScale: 1.0 - 0.2 * disappearingAnimationProgress
151 opacity: 1.0 - disappearingAnimationProgress
152 property real disappearingAnimationProgress: {
153 if (stages.overlayMode) {
154 return 0;
155 } else {
156 return stages.showProgress
157 }
158 }
160 // FIXME: only necessary because stages.showProgress is not animated
161 Behavior on disappearingAnimationProgress { SmoothedAnimation { velocity: 5 }}
162 }
163 }
164 }
166 EdgeDragArea {
167 id: stagesDragHandle
168 direction: Direction.Leftwards
170 anchors { top: parent.top; right: parent.right; bottom: parent.bottom }
171 width: shell.edgeSize
173 property real progress: stages.width
175 onTouchXChanged: {
176 if (status == DirectionalDragArea.Recognized) {
177 if (ApplicationManager.count == 0) {
178 progress = Math.max(stages.width - stagesDragHandle.width + touchX, stages.width * .3)
179 } else {
180 progress = stages.width - stagesDragHandle.width + touchX
181 }
182 }
183 }
185 onDraggingChanged: {
186 if (!dragging) {
187 if (ApplicationManager.count > 0 && progress < stages.width - units.gu(10)) {
188 stages.show(true)
189 }
190 stagesDragHandle.progress = stages.width;
191 }
192 }
193 }
195 Item {
196 id: stages
197 objectName: "stages"
198 width: parent.width
199 height: parent.height
201 x: {
202 if (shown) {
203 if (overlayMode || locked) {
204 return 0;
205 }
206 return launcher.progress
207 } else {
208 return stagesDragHandle.progress
209 }
210 }
212 Behavior on x { SmoothedAnimation { velocity: 600; duration: UbuntuAnimation.FastDuration } }
214 property bool shown: false
216 property real showProgress: overlayMode ? 0 : MathUtils.clamp(1 - x / shell.width, 0, 1)
218 property bool fullyShown: x == 0
219 property bool fullyHidden: x == width
221 property bool painting: applicationsDisplayLoader.item ? applicationsDisplayLoader.item.painting : false
222 property bool fullscreen: applicationsDisplayLoader.item ? applicationsDisplayLoader.item.fullscreen : false
223 property bool overlayMode: applicationsDisplayLoader.item ? applicationsDisplayLoader.item.overlayMode : false
224 property int overlayWidth: applicationsDisplayLoader.item ? applicationsDisplayLoader.item.overlayWidth : false
225 property bool locked: applicationsDisplayLoader.item ? applicationsDisplayLoader.item.locked : false
227 function show(focusApp) {
228 shown = true;
229 panel.indicators.hide();
230 edgeDemo.stopDemo();
231 greeter.hide();
232 if (!ApplicationManager.focusedApplicationId && ApplicationManager.count > 0 && focusApp) {
233 ApplicationManager.focusApplication(ApplicationManager.get(0).appId);
234 }
235 }
237 function hide() {
238 shown = false;
239 if (ApplicationManager.focusedApplicationId) {
240 ApplicationManager.unfocusCurrentApplication();
241 }
242 }
244 Connections {
245 target: ApplicationManager
247 onFocusRequested: {
248 stages.show(true);
249 }
251 onFocusedApplicationIdChanged: {
252 if (ApplicationManager.focusedApplicationId.length > 0) {
253 stages.show(false);
254 } else {
255 if (!stages.overlayMode) {
256 stages.hide();
257 }
258 }
259 }
261 onApplicationAdded: {
262 stages.show(false);
263 }
265 onApplicationRemoved: {
266 if (ApplicationManager.focusedApplicationId.length == 0) {
267 stages.hide();
268 }
269 }
270 }
272 property bool dialogShown: false
274 Component {
275 id: logoutDialog
276 Dialog {
277 id: dialogueLogout
278 title: "Logout"
279 text: "Are you sure that you want to logout?"
280 Button {
281 text: "Cancel"
282 onClicked: {
283 PopupUtils.close(dialogueLogout);
284 stages.dialogShown = false;
285 }
286 }
287 Button {
288 text: "Yes"
289 onClicked: {
290 DBusUnitySessionService.Logout();
291 PopupUtils.close(dialogueLogout);
292 stages.dialogShown = false;
293 }
294 }
295 }
296 }
298 Component {
299 id: shutdownDialog
300 Dialog {
301 id: dialogueShutdown
302 title: "Shutdown"
303 text: "Are you sure that you want to shutdown?"
304 Button {
305 text: "Cancel"
306 onClicked: {
307 PopupUtils.close(dialogueShutdown);
308 stages.dialogShown = false;
309 }
310 }
311 Button {
312 text: "Yes"
313 onClicked: {
314 dBusUnitySessionServiceConnection.closeAllApps();
315 DBusUnitySessionService.Shutdown();
316 PopupUtils.close(dialogueShutdown);
317 stages.dialogShown = false;
318 }
319 }
320 }
321 }
323 Component {
324 id: rebootDialog
325 Dialog {
326 id: dialogueReboot
327 title: "Reboot"
328 text: "Are you sure that you want to reboot?"
329 Button {
330 text: "Cancel"
331 onClicked: {
332 PopupUtils.close(dialogueReboot)
333 stages.dialogShown = false;
334 }
335 }
336 Button {
337 text: "Yes"
338 onClicked: {
339 dBusUnitySessionServiceConnection.closeAllApps();
340 DBusUnitySessionService.Reboot();
341 PopupUtils.close(dialogueReboot);
342 stages.dialogShown = false;
343 }
344 }
345 }
346 }
348 Component {
349 id: powerDialog
350 Dialog {
351 id: dialoguePower
352 title: "Power"
353 text: i18n.tr("Are you sure you would like to turn power off?")
354 Button {
355 text: i18n.tr("Power off")
356 onClicked: {
357 dBusUnitySessionServiceConnection.closeAllApps();
358 PopupUtils.close(dialoguePower);
359 stages.dialogShown = false;
360 shutdownFadeOutRectangle.enabled = true;
361 shutdownFadeOutRectangle.visible = true;
362 shutdownFadeOut.start();
363 }
364 }
365 Button {
366 text: i18n.tr("Restart")
367 onClicked: {
368 dBusUnitySessionServiceConnection.closeAllApps();
369 DBusUnitySessionService.Reboot();
370 PopupUtils.close(dialoguePower);
371 stages.dialogShown = false;
372 }
373 }
374 Button {
375 text: i18n.tr("Cancel")
376 onClicked: {
377 PopupUtils.close(dialoguePower);
378 stages.dialogShown = false;
379 }
380 }
381 }
382 }
384 function showPowerDialog() {
385 if (!stages.dialogShown) {
386 stages.dialogShown = true;
387 PopupUtils.open(powerDialog);
388 }
389 }
391 Connections {
392 id: dBusUnitySessionServiceConnection
393 objectName: "dBusUnitySessionServiceConnection"
394 target: DBusUnitySessionService
396 function closeAllApps() {
397 while (true) {
398 var app = ApplicationManager.get(0);
399 if (app === null) {
400 break;
401 }
402 ApplicationManager.stopApplication(app.appId);
403 }
404 }
406 onLogoutRequested: {
407 // Display a dialog to ask the user to confirm.
408 if (!stages.dialogShown) {
409 stages.dialogShown = true;
410 PopupUtils.open(logoutDialog);
411 }
412 }
414 onShutdownRequested: {
415 // Display a dialog to ask the user to confirm.
416 if (!stages.dialogShown) {
417 stages.dialogShown = true;
418 PopupUtils.open(shutdownDialog);
419 }
420 }
422 onRebootRequested: {
423 // Display a dialog to ask the user to confirm.
424 if (!stages.dialogShown) {
425 stages.dialogShown = true;
426 PopupUtils.open(rebootDialog);
427 }
428 }
430 onLogoutReady: {
431 closeAllApps();
432 Qt.quit();
433 }
434 }
436 Loader {
437 id: applicationsDisplayLoader
438 anchors.fill: parent
440 source: shell.sideStageEnabled ? "Stages/StageWithSideStage.qml" : "Stages/PhoneStage.qml"
442 Binding {
443 target: applicationsDisplayLoader.item
444 property: "moving"
445 value: !stages.fullyShown
446 }
447 Binding {
448 target: applicationsDisplayLoader.item
449 property: "shown"
450 value: stages.shown
451 }
452 Binding {
453 target: applicationsDisplayLoader.item
454 property: "dragAreaWidth"
455 value: shell.edgeSize
456 }
457 }
458 }
460 Lockscreen {
461 id: lockscreen
462 objectName: "lockscreen"
464 readonly property int backgroundTopMargin: -panel.panelHeight
466 hides: [launcher, panel.indicators]
467 shown: false
468 enabled: true
469 showAnimation: StandardAnimation { property: "opacity"; to: 1 }
470 hideAnimation: StandardAnimation { property: "opacity"; to: 0 }
471 y: panel.panelHeight
472 x: required ? 0 : - width
473 width: parent.width
474 height: parent.height - panel.panelHeight
475 background: shell.background
476 minPinLength: 4
477 maxPinLength: 4
479 onEntered: LightDM.Greeter.respond(passphrase);
480 onCancel: greeter.show()
482 Component.onCompleted: {
483 if (LightDM.Users.count == 1) {
484 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
485 }
486 }
487 }
489 Connections {
490 target: LightDM.Greeter
492 onShowGreeter: greeter.show()
494 onShowPrompt: {
495 if (LightDM.Users.count == 1) {
496 // TODO: There's no better way for now to determine if its a PIN or a passphrase.
497 if (text == "PIN") {
498 lockscreen.alphaNumeric = false
499 } else {
500 lockscreen.alphaNumeric = true
501 }
502 lockscreen.placeholderText = i18n.tr("Please enter %1").arg(text);
503 lockscreen.show();
504 }
505 }
507 onAuthenticationComplete: {
508 if (LightDM.Greeter.promptless) {
509 return;
510 }
511 if (LightDM.Greeter.authenticated) {
512 lockscreen.hide();
513 } else {
514 lockscreen.clear(true);
515 }
516 }
517 }
519 Binding {
520 target: LightDM.Greeter
521 property: "active"
522 value: greeter.shown || lockscreen.shown
523 }
525 Rectangle {
526 anchors.fill: parent
527 color: "black"
528 opacity: greeterWrapper.showProgress * 0.8
529 }
531 Item {
532 // Just a tiny wrapper to adjust greeter's x without messing with its own dragging
533 id: greeterWrapper
534 x: launcher.progress
535 y: panel.panelHeight
536 width: parent.width
537 height: parent.height - panel.panelHeight
539 Behavior on x {
540 enabled: !launcher.dashSwipe
541 StandardAnimation {}
542 }
544 readonly property real showProgress: MathUtils.clamp((1 - x/width) + greeter.showProgress - 1, 0, 1)
546 Greeter {
547 id: greeter
548 objectName: "greeter"
550 available: true
551 hides: [launcher, panel.indicators]
552 shown: true
554 defaultBackground: shell.background
556 width: parent.width
557 height: parent.height
559 dragHandleWidth: shell.edgeSize
561 onShownChanged: {
562 if (shown) {
563 lockscreen.reset();
564 if (!LightDM.Greeter.promptless) {
565 lockscreen.show();
566 }
567 // If there is only one user, we start authenticating with that one here.
568 // If there are more users, the Greeter will handle that
569 if (LightDM.Users.count == 1) {
570 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
571 }
572 greeter.forceActiveFocus();
573 }
574 }
576 onUnlocked: greeter.hide()
577 onSelected: {
578 // Update launcher items for new user
579 var user = LightDM.Users.data(uid, LightDM.UserRoles.NameRole);
580 AccountsService.user = user;
581 LauncherModel.setUser(user);
582 }
584 onTease: launcher.tease()
586 Binding {
587 target: ApplicationManager
588 property: "suspended"
589 value: greeter.shown && greeterWrapper.showProgress == 1
590 }
591 }
592 }
594 InputFilterArea {
595 anchors.fill: parent
596 blockInput: ApplicationManager.focusedApplicationId.length === 0 || greeter.shown || lockscreen.shown || launcher.shown
597 || panel.indicators.shown
598 }
600 Connections {
601 id: powerConnection
602 target: Powerd
604 onDisplayPowerStateChange: {
605 // We ignore any display-off signals when the proximity sensor
606 // is active. This usually indicates something like a phone call.
607 if (status == Powerd.Off && reason != Powerd.Proximity) {
608 greeter.showNow();
609 }
611 // No reason to chew demo CPU when user isn't watching
612 if (status == Powerd.Off) {
613 edgeDemo.paused = true;
614 } else if (status == Powerd.On) {
615 edgeDemo.paused = false;
616 }
617 }
618 }
620 function showHome() {
621 var animate = !greeter.shown && !stages.shown
622 greeter.hide()
623 dash.setCurrentScope("clickscope", animate, false)
624 stages.hide()
625 }
627 function hideIndicatorMenu(delay) {
628 panel.hideIndicatorMenu(delay);
629 }
631 Item {
632 id: overlay
634 anchors.fill: parent
636 Panel {
637 id: panel
638 anchors.fill: parent //because this draws indicator menus
639 indicators {
640 hides: [launcher]
641 available: edgeDemo.panelEnabled
642 contentEnabled: edgeDemo.panelContentEnabled
643 width: parent.width > units.gu(60) ? units.gu(40) : parent.width
644 panelHeight: units.gu(3)
645 }
646 property string focusedAppId: ApplicationManager.focusedApplicationId
647 property var focusedApplication: ApplicationManager.findApplication(focusedAppId)
648 fullscreenMode: focusedApplication && stages.fullscreen && !greeter.shown && !lockscreen.shown
650 InputFilterArea {
651 anchors {
652 top: parent.top
653 left: parent.left
654 right: parent.right
655 }
656 height: (panel.fullscreenMode) ? shell.edgeSize : panel.panelHeight
657 blockInput: true
658 }
659 }
661 InputFilterArea {
662 blockInput: launcher.shown
663 anchors {
664 top: parent.top
665 bottom: parent.bottom
666 left: parent.left
667 }
668 width: launcher.width
669 }
671 Launcher {
672 id: launcher
674 readonly property bool dashSwipe: progress > 0
676 anchors.top: parent.top
677 anchors.bottom: parent.bottom
678 width: parent.width
679 dragAreaWidth: shell.edgeSize
680 available: edgeDemo.launcherEnabled
682 onShowDashHome: {
683 if (edgeDemo.running)
684 return;
686 showHome()
687 }
688 onDash: {
689 if (stages.shown && !stages.overlayMode && !stages.locked) {
690 stages.hide();
691 launcher.fadeOut();
692 } else {
693 launcher.switchToNextState("visible");
694 }
696 if (greeter.shown) {
697 greeter.hideRight();
698 launcher.fadeOut();
699 }
700 }
701 onDashSwipeChanged: if (dashSwipe && stages.shown) dash.setCurrentScope("clickscope", false, true)
702 onLauncherApplicationSelected: {
703 if (!edgeDemo.running)
704 shell.activateApplication(appId)
705 }
706 onShownChanged: {
707 if (shown) {
708 panel.indicators.hide()
709 }
710 }
711 }
713 Rectangle {
714 id: modalNotificationBackground
716 visible: notifications.useModal && !greeter.shown && (notifications.state == "narrow")
717 color: "#000000"
718 anchors.fill: parent
719 opacity: 0.5
721 MouseArea {
722 anchors.fill: parent
723 }
725 InputFilterArea {
726 anchors.fill: parent
727 blockInput: modalNotificationBackground.visible
728 }
729 }
731 Notifications {
732 id: notifications
734 model: NotificationBackend.Model
735 margin: units.gu(1)
737 y: panel.panelHeight
738 width: parent.width
739 height: parent.height - panel.panelHeight
741 states: [
742 State {
743 name: "narrow"
744 when: overlay.width <= units.gu(60)
745 AnchorChanges { target: notifications; anchors.left: parent.left }
746 },
747 State {
748 name: "wide"
749 when: overlay.width > units.gu(60)
750 AnchorChanges { target: notifications; anchors.left: undefined }
751 PropertyChanges { target: notifications; width: units.gu(38) }
752 }
753 ]
755 InputFilterArea {
756 anchors { left: parent.left; right: parent.right }
757 height: parent.contentHeight
758 blockInput: height > 0
759 }
760 }
761 }
763 focus: true
764 onFocusChanged: if (!focus) forceActiveFocus();
766 InputFilterArea {
767 anchors {
768 top: parent.top
769 bottom: parent.bottom
770 left: parent.left
771 }
772 width: shell.edgeSize
773 blockInput: true
774 }
776 InputFilterArea {
777 anchors {
778 top: parent.top
779 bottom: parent.bottom
780 right: parent.right
781 }
782 width: shell.edgeSize
783 blockInput: true
784 }
786 Binding {
787 target: i18n
788 property: "domain"
789 value: "unity8"
790 }
792 OSKController {
793 anchors.topMargin: panel.panelHeight
794 anchors.fill: parent // as needs to know the geometry of the shell
795 }
797 //FIXME: This should be handled in the input stack, keyboard shouldnt propagate
798 MouseArea {
799 anchors.bottom: parent.bottom
800 anchors.left: parent.left
801 anchors.right: parent.right
802 height: ApplicationManager.keyboardVisible ? ApplicationManager.keyboardHeight : 0
804 enabled: ApplicationManager.keyboardVisible
805 }
807 Label {
808 anchors.centerIn: parent
809 visible: ApplicationManager.fake ? ApplicationManager.fake : false
810 text: "EARLY ALPHA\nNOT READY FOR USE"
811 color: "lightgrey"
812 opacity: 0.2
813 font.weight: Font.Black
814 horizontalAlignment: Text.AlignHCenter
815 verticalAlignment: Text.AlignVCenter
816 fontSizeMode: Text.Fit
817 rotation: -45
818 scale: Math.min(parent.width, parent.height) / width
819 }
821 EdgeDemo {
822 id: edgeDemo
823 greeter: greeter
824 launcher: launcher
825 dash: dash
826 indicators: panel.indicators
827 underlay: underlay
828 }
830 Connections {
831 target: SessionBroadcast
832 onShowHome: showHome()
833 }
835 Keys.onPressed: {
836 if (event.key == Qt.Key_PowerOff || event.key == Qt.Key_PowerDown) {
837 if (!powerKeyTimer.running) {
838 powerKeyTimer.start();
839 }
840 event.accepted = true;
841 }
842 }
844 Keys.onReleased: {
845 if (event.key == Qt.Key_PowerOff || event.key == Qt.Key_PowerDown) {
846 powerKeyTimer.stop();
847 event.accepted = true;
848 }
849 }
851 Timer {
852 id: powerKeyTimer
853 interval: 2000
854 repeat: false
855 triggeredOnStart: false
857 onTriggered: {
858 stages.showPowerDialog();
859 }
860 }
862 Rectangle {
863 id: shutdownFadeOutRectangle
864 enabled: false
865 visible: false
866 color: "black"
867 anchors.fill: parent
868 opacity: 0.0
869 NumberAnimation on opacity {
870 id: shutdownFadeOut
871 from: 0.0
872 to: 1.0
873 onStopped: {
874 if (shutdownFadeOutRectangle.enabled && shutdownFadeOutRectangle.visible) {
875 DBusUnitySessionService.Shutdown();
876 }
877 }
878 }
879 }