on xenial help sign in
clean patch 2
commitf0c271d979faf8b29f7095e4b2f7bc465b126761 authorLukáš Tinkl <lukas.tinkl@canonical.com> dateTue 23 Aug 2016, 15:53 (10 years ago) parent4a6748f
13 files changed+699−131
plugins/Utils/windowstatestorage.cpp+5−4 plugins/Utils/windowstatestorage.h+8−3 qml/Components/WindowControlButtons.qml−1 qml/Stages/DecoratedWindow.qml+20 qml/Stages/DesktopStage.qml+140−67 qml/Stages/FakeMaximizeDelegate.qml+186 qml/Stages/WindowControlsOverlay.qml+158−16 qml/Stages/WindowDecoration.qml+150−8 qml/Stages/WindowResizeArea.qml+11−24 tests/mocks/Utils/windowstatestorage.cpp+2−2 tests/mocks/Utils/windowstatestorage.h+8−3 tests/qmltests/Stages/tst_DesktopStage.qml+2−2 tests/qmltests/Stages/tst_WindowResizeArea.qml+9−1
plugins/Utils/windowstatestorage.cpp+5−4
@@ -76,7 +76,7 @@ WindowStateStorage::WindowState WindowStateStorage::getState(const QString &wind
7676 return (WindowState)query.value(QStringLiteral("state")).toInt();
7777}
7878
79−void WindowStateStorage::saveGeometry(const QString &windowId, const QRect rect)
79+void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect)
8080{
8181 const QString queryString = QStringLiteral("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
8282 .arg(sanitiseString(windowId))
@@ -92,7 +92,7 @@ void WindowStateStorage::saveStage(const QString &appId, int stage)
9292{
9393 const QString queryString = QStringLiteral("INSERT OR REPLACE INTO stage (appId, stage) values ('%1', '%2');")
9494 .arg(sanitiseString(appId))
95− .arg((int)stage);
95+ .arg(stage);
9696
9797 saveValue(queryString);
9898}
@@ -123,7 +123,7 @@ void WindowStateStorage::executeAsyncQuery(const QString &queryString)
123123 }
124124}
125125
126−QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect defaultValue) const
126+QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue) const
127127{
128128 QString queryString = QStringLiteral("SELECT * FROM geometry WHERE windowId = '%1';")
129129 .arg(sanitiseString(windowId));
@@ -133,7 +133,8 @@ QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect defau
133133 if (!query.first()) {
134134 return defaultValue;
135135 }
136− return QRect(query.value(QStringLiteral("x")).toInt(), query.value(QStringLiteral("y")).toInt(), query.value(QStringLiteral("width")).toInt(), query.value(QStringLiteral("height")).toInt());
136+ return QRect(query.value(QStringLiteral("x")).toInt(), query.value(QStringLiteral("y")).toInt(),
137+ query.value(QStringLiteral("width")).toInt(), query.value(QStringLiteral("height")).toInt());
137138}
138139
139140void WindowStateStorage::initdb()
plugins/Utils/windowstatestorage.h+8−3
@@ -32,7 +32,12 @@ public:
3232 WindowStateMaximizedLeft = 1 << 4,
3333 WindowStateMaximizedRight = 1 << 5,
3434 WindowStateMaximizedHorizontally = 1 << 6,
35− WindowStateMaximizedVertically = 1 << 7
35+ WindowStateMaximizedVertically = 1 << 7,
36+ WindowStateMaximizedTopLeft = 1 << 8,
37+ WindowStateMaximizedTopRight = 1 << 9,
38+ WindowStateMaximizedBottomLeft = 1 << 10,
39+ WindowStateMaximizedBottomRight = 1 << 11,
40+ WindowStateRestored = 1 << 12
3641 };
3742 Q_DECLARE_FLAGS(WindowStates, WindowState)
3843#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
@@ -45,8 +50,8 @@ public:
4550 Q_INVOKABLE void saveState(const QString &windowId, WindowState state);
4651 Q_INVOKABLE WindowState getState(const QString &windowId, WindowState defaultValue) const;
4752
48− Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect rect);
49− Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect defaultValue) const;
53+ Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
54+ Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue) const;
5055
5156 Q_INVOKABLE void saveStage(const QString &appId, int stage);
5257 Q_INVOKABLE int getStage(const QString &appId, int defaultValue) const;
qml/Components/WindowControlButtons.qml−1
@@ -25,7 +25,6 @@ Row {
2525 }
2626
2727 // to be set from outside
28− property Item target
2928 property bool active: false
3029 property bool windowIsMaximized: false
3130 property bool closeButtonShown: true
qml/Stages/DecoratedWindow.qml+20
@@ -52,6 +52,8 @@ FocusScope {
5252 readonly property int heightIncrement: !counterRotate ? applicationWindow.heightIncrement : applicationWindow.widthIncrement
5353
5454 property alias overlayShown: decoration.overlayShown
55+ property alias stageWidth: decoration.stageWidth
56+ property alias stageHeight: decoration.stageHeight
5557
5658 signal closeClicked()
5759 signal maximizeClicked()
@@ -60,6 +62,15 @@ FocusScope {
6062 signal minimizeClicked()
6163 signal decorationPressed()
6264
65+ signal fakeMaximizeAnimationRequested(real progress)
66+ signal fakeMaximizeLeftAnimationRequested(real progress)
67+ signal fakeMaximizeRightAnimationRequested(real progress)
68+ signal fakeMaximizeTopLeftAnimationRequested(real progress)
69+ signal fakeMaximizeTopRightAnimationRequested(real progress)
70+ signal fakeMaximizeBottomLeftAnimationRequested(real progress)
71+ signal fakeMaximizeBottomRightAnimationRequested(real progress)
72+ signal stopFakeAnimation()
73+
6374 Rectangle {
6475 id: selectionHighlight
6576 anchors.fill: parent
@@ -101,6 +112,15 @@ FocusScope {
101112 onMaximizeVerticallyClicked: { root.decorationPressed(); root.maximizeVerticallyClicked(); }
102113 onMinimizeClicked: root.minimizeClicked();
103114 onPressed: root.decorationPressed();
115+
116+ onFakeMaximizeAnimationRequested: root.fakeMaximizeAnimationRequested(progress)
117+ onFakeMaximizeLeftAnimationRequested: root.fakeMaximizeLeftAnimationRequested(progress)
118+ onFakeMaximizeRightAnimationRequested: root.fakeMaximizeRightAnimationRequested(progress)
119+ onFakeMaximizeTopLeftAnimationRequested: root.fakeMaximizeTopLeftAnimationRequested(progress)
120+ onFakeMaximizeTopRightAnimationRequested: root.fakeMaximizeTopRightAnimationRequested(progress)
121+ onFakeMaximizeBottomLeftAnimationRequested: root.fakeMaximizeBottomLeftAnimationRequested(progress)
122+ onFakeMaximizeBottomRightAnimationRequested: root.fakeMaximizeBottomRightAnimationRequested(progress)
123+ onStopFakeAnimation: root.stopFakeAnimation()
104124 }
105125
106126 ApplicationWindow {
qml/Stages/DesktopStage.qml+140−67
@@ -68,28 +68,27 @@ AbstractStage {
6868 id: maximizeWindowShortcut
6969 shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Up
7070 onTriggered: priv.focusedAppDelegate.maximize()
71− active: priv.focusedAppDelegate !== null
71+ active: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.canBeMaximized
7272 }
7373
7474 GlobalShortcut {
7575 id: maximizeWindowLeftShortcut
7676 shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Left
7777 onTriggered: priv.focusedAppDelegate.maximizeLeft()
78− active: priv.focusedAppDelegate !== null
78+ active: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.canBeMaximizedLeftRight
7979 }
8080
8181 GlobalShortcut {
8282 id: maximizeWindowRightShortcut
8383 shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Right
8484 onTriggered: priv.focusedAppDelegate.maximizeRight()
85− active: priv.focusedAppDelegate !== null
85+ active: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.canBeMaximizedLeftRight
8686 }
8787
8888 GlobalShortcut {
8989 id: minimizeRestoreShortcut
9090 shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Down
91− onTriggered: priv.focusedAppDelegate.maximized || priv.focusedAppDelegate.maximizedLeft || priv.focusedAppDelegate.maximizedRight ||
92− priv.focusedAppDelegate.maximizedHorizontally || priv.focusedAppDelegate.maximizedVertically
91+ onTriggered: priv.focusedAppDelegate.anyMaximized
9392 ? priv.focusedAppDelegate.restoreFromMaximized() : priv.focusedAppDelegate.minimize()
9493 active: priv.focusedAppDelegate !== null
9594 }
@@ -276,7 +275,7 @@ AbstractStage {
276275 value: appDelegate.requestedY -
277276 Math.min(appDelegate.requestedY - PanelState.panelHeight,
278277 Math.max(0, priv.virtualKeyboardHeight - (appContainer.height - (appDelegate.requestedY + appDelegate.height))))
279− when: root.oskEnabled && appDelegate.focus && appDelegate.state == "normal"
278+ when: root.oskEnabled && appDelegate.focus && (appDelegate.state == "normal" || appDelegate.state == "restored")
280279 && SurfaceManager.inputMethodSurface
281280 && SurfaceManager.inputMethodSurface.state != Mir.HiddenState
282281 && SurfaceManager.inputMethodSurface.state != Mir.MinimizedState
@@ -291,7 +290,7 @@ AbstractStage {
291290 onShellOrientationAngleChanged: {
292291 // at this point decoratedWindow.surfaceOrientationAngle is the old shellOrientationAngle
293292 if (application && application.rotatesWindowContents) {
294− if (state == "normal") {
293+ if (state == "normal" || state == "restored") {
295294 var angleDiff = decoratedWindow.surfaceOrientationAngle - shellOrientationAngle;
296295 angleDiff = (360 + angleDiff) % 360;
297296 if (angleDiff === 90 || angleDiff === 270) {
@@ -317,14 +316,29 @@ AbstractStage {
317316 property int requestedWidth: -1
318317 property int requestedHeight: -1
319318
320− readonly property bool maximized: windowState & WindowStateStorage.WindowStateMaximized
321− readonly property bool maximizedLeft: windowState & WindowStateStorage.WindowStateMaximizedLeft
322− readonly property bool maximizedRight: windowState & WindowStateStorage.WindowStateMaximizedRight
323− readonly property bool maximizedHorizontally: windowState & WindowStateStorage.WindowStateMaximizedHorizontally
324− readonly property bool maximizedVertically: windowState & WindowStateStorage.WindowStateMaximizedVertically
319+ readonly property bool maximized: windowState === WindowStateStorage.WindowStateMaximized
320+ readonly property bool maximizedLeft: windowState === WindowStateStorage.WindowStateMaximizedLeft
321+ readonly property bool maximizedRight: windowState === WindowStateStorage.WindowStateMaximizedRight
322+ readonly property bool maximizedHorizontally: windowState === WindowStateStorage.WindowStateMaximizedHorizontally
323+ readonly property bool maximizedVertically: windowState === WindowStateStorage.WindowStateMaximizedVertically
324+ readonly property bool maximizedTopLeft: windowState === WindowStateStorage.WindowStateMaximizedTopLeft
325+ readonly property bool maximizedTopRight: windowState === WindowStateStorage.WindowStateMaximizedTopRight
326+ readonly property bool maximizedBottomLeft: windowState === WindowStateStorage.WindowStateMaximizedBottomLeft
327+ readonly property bool maximizedBottomRight: windowState === WindowStateStorage.WindowStateMaximizedBottomRight
328+ readonly property bool anyMaximized: maximized || maximizedLeft || maximizedRight || maximizedHorizontally || maximizedVertically ||
329+ maximizedTopLeft || maximizedTopRight || maximizedBottomLeft || maximizedBottomRight
330+
325331 readonly property bool minimized: windowState & WindowStateStorage.WindowStateMinimized
326332 readonly property alias fullscreen: decoratedWindow.fullscreen
327333
334+ readonly property bool canBeMaximized: canBeMaximizedHorizontally && canBeMaximizedVertically
335+ readonly property bool canBeMaximizedLeftRight: (maximumWidth == 0 || maximumWidth >= appContainer.width/2) &&
336+ (maximumHeight == 0 || maximumHeight >= appContainer.height)
337+ readonly property bool canBeCornerMaximized: (maximumWidth == 0 || maximumWidth >= appContainer.width/2) &&
338+ (maximumHeight == 0 || maximumHeight >= appContainer.height/2)
339+ readonly property bool canBeMaximizedHorizontally: maximumWidth == 0 || maximumWidth >= appContainer.width
340+ readonly property bool canBeMaximizedVertically: maximumHeight == 0 || maximumHeight >= appContainer.height
341+
328342 property int windowState: WindowStateStorage.WindowStateNormal
329343 property bool animationsEnabled: true
330344 property alias title: decoratedWindow.title
@@ -342,7 +356,7 @@ AbstractStage {
342356 if (spread.state == "altTab") {
343357 spread.cancel();
344358 }
345− appDelegate.restore();
359+ appDelegate.restore(true /* animated */, appDelegate.windowState);
346360 }
347361 Connections {
348362 target: model.surface
@@ -362,9 +376,6 @@ AbstractStage {
362376 }
363377
364378 onFocusChanged: {
365− if (appRepeater.startingUp)
366− return;
367−
368379 if (focus) {
369380 // If we're orphan (!parent) it means this stage is no longer the current one
370381 // and will be deleted shortly. So we should no longer have a say over the model
@@ -450,28 +461,34 @@ AbstractStage {
450461 animationsEnabled = (animated === undefined) || animated;
451462 windowState = WindowStateStorage.WindowStateMaximizedVertically;
452463 }
464+ function maximizeTopLeft(animated) {
465+ animationsEnabled = (animated === undefined) || animated;
466+ windowState = WindowStateStorage.WindowStateMaximizedTopLeft;
467+ }
468+ function maximizeTopRight(animated) {
469+ animationsEnabled = (animated === undefined) || animated;
470+ windowState = WindowStateStorage.WindowStateMaximizedTopRight;
471+ }
472+ function maximizeBottomLeft(animated) {
473+ animationsEnabled = (animated === undefined) || animated;
474+ windowState = WindowStateStorage.WindowStateMaximizedBottomLeft;
475+ }
476+ function maximizeBottomRight(animated) {
477+ animationsEnabled = (animated === undefined) || animated;
478+ windowState = WindowStateStorage.WindowStateMaximizedBottomRight;
479+ }
453480 function minimize(animated) {
454481 animationsEnabled = (animated === undefined) || animated;
455482 windowState |= WindowStateStorage.WindowStateMinimized; // add the minimized bit
456483 }
457484 function restoreFromMaximized(animated) {
458485 animationsEnabled = (animated === undefined) || animated;
459− windowState = WindowStateStorage.WindowStateNormal;
486+ windowState = WindowStateStorage.WindowStateRestored;
460487 }
461− function restore(animated) {
488+ function restore(animated,state) {
462489 animationsEnabled = (animated === undefined) || animated;
490+ windowState = state || WindowStateStorage.WindowStateRestored;
463491 windowState &= ~WindowStateStorage.WindowStateMinimized; // clear the minimized bit
464− if (maximized)
465− maximize();
466− else if (maximizedLeft)
467− maximizeLeft();
468− else if (maximizedRight)
469− maximizeRight();
470− else if (maximizedHorizontally)
471− maximizeHorizontally();
472− else if (maximizedVertically)
473− maximizeVertically();
474−
475492 focus = true;
476493 }
477494
@@ -488,9 +505,12 @@ AbstractStage {
488505 duration: UbuntuAnimation.SnapDuration
489506 }
490507
508+ property real restoredX
509+ property real restoredY
510+
491511 states: [
492512 State {
493− name: "fullscreen"; when: decoratedWindow.fullscreen && !appDelegate.minimized
513+ name: "fullscreen"; when: appDelegate.fullscreen && !appDelegate.minimized
494514 PropertyChanges {
495515 target: appDelegate;
496516 x: rotation == 0 ? 0 : (parent.width - width) / 2 + (shellOrientationAngle == 90 ? -PanelState.panelHeight : PanelState.panelHeight)
@@ -509,61 +529,94 @@ AbstractStage {
509529 }
510530 },
511531 State {
512− name: "maximized"; when: appDelegate.maximized && !appDelegate.minimized
532+ name: "restored";
533+ when: appDelegate.windowState == WindowStateStorage.WindowStateRestored
534+ extend: "normal"
513535 PropertyChanges {
514536 target: appDelegate;
515− x: root.leftMargin;
516− y: 0;
517− visuallyMinimized: false;
518− visuallyMaximized: true
537+ requestedX: restoredX;
538+ requestedY: restoredY;
519539 }
540+ },
541+ State {
542+ name: "maximized"; when: appDelegate.maximized && !appDelegate.minimized
520543 PropertyChanges {
521− target: decoratedWindow
544+ target: appDelegate;
545+ requestedX: root.leftMargin;
546+ requestedY: 0;
522547 requestedWidth: appContainer.width - root.leftMargin;
523548 requestedHeight: appContainer.height;
549+ visuallyMinimized: false;
550+ visuallyMaximized: true
524551 }
525552 },
526553 State {
527554 name: "maximizedLeft"; when: appDelegate.maximizedLeft && !appDelegate.minimized
528555 PropertyChanges {
529556 target: appDelegate
530− x: root.leftMargin
531− y: PanelState.panelHeight
532− }
533− PropertyChanges {
534− target: decoratedWindow
557+ requestedX: root.leftMargin
558+ requestedY: PanelState.panelHeight
535559 requestedWidth: (appContainer.width - root.leftMargin)/2
536560 requestedHeight: appContainer.height - PanelState.panelHeight
537561 }
538562 },
539563 State {
540564 name: "maximizedRight"; when: appDelegate.maximizedRight && !appDelegate.minimized
565+ extend: "maximizedLeft"
541566 PropertyChanges {
542567 target: appDelegate;
543− x: (appContainer.width + root.leftMargin)/2
544− y: PanelState.panelHeight
568+ requestedX: (appContainer.width + root.leftMargin)/2
545569 }
570+ },
571+ State {
572+ name: "maximizedTopLeft"; when: appDelegate.maximizedTopLeft && !appDelegate.minimized
546573 PropertyChanges {
547− target: decoratedWindow
574+ target: appDelegate
575+ requestedX: root.leftMargin
576+ requestedY: PanelState.panelHeight
548577 requestedWidth: (appContainer.width - root.leftMargin)/2
549− requestedHeight: appContainer.height - PanelState.panelHeight
578+ requestedHeight: (appContainer.height - PanelState.panelHeight)/2
579+ }
580+ },
581+ State {
582+ name: "maximizedTopRight"; when: appDelegate.maximizedTopRight && !appDelegate.minimized
583+ extend: "maximizedTopLeft"
584+ PropertyChanges {
585+ target: appDelegate
586+ requestedX: (appContainer.width + root.leftMargin)/2
587+ }
588+ },
589+ State {
590+ name: "maximizedBottomLeft"; when: appDelegate.maximizedBottomLeft && !appDelegate.minimized
591+ PropertyChanges {
592+ target: appDelegate
593+ requestedX: root.leftMargin
594+ requestedY: (appContainer.height + PanelState.panelHeight)/2
595+ requestedWidth: (appContainer.width - root.leftMargin)/2
596+ requestedHeight: appContainer.height/2
597+ }
598+ },
599+ State {
600+ name: "maximizedBottomRight"; when: appDelegate.maximizedBottomRight && !appDelegate.minimized
601+ extend: "maximizedBottomLeft"
602+ PropertyChanges {
603+ target: appDelegate
604+ requestedX: (appContainer.width + root.leftMargin)/2
550605 }
551606 },
552607 State {
553608 name: "maximizedHorizontally"; when: appDelegate.maximizedHorizontally && !appDelegate.minimized
554− PropertyChanges { target: appDelegate; x: root.leftMargin }
555− PropertyChanges { target: decoratedWindow; requestedWidth: appContainer.width - root.leftMargin }
609+ PropertyChanges { target: appDelegate; requestedX: root.leftMargin; requestedY: requestedY; requestedWidth: appContainer.width - root.leftMargin }
556610 },
557611 State {
558612 name: "maximizedVertically"; when: appDelegate.maximizedVertically && !appDelegate.minimized
559− PropertyChanges { target: appDelegate; y: PanelState.panelHeight }
560− PropertyChanges { target: decoratedWindow; requestedHeight: appContainer.height - PanelState.panelHeight }
613+ PropertyChanges { target: appDelegate; requestedX: requestedX; requestedY: PanelState.panelHeight; requestedHeight: appContainer.height - PanelState.panelHeight }
561614 },
562615 State {
563616 name: "minimized"; when: appDelegate.minimized
564617 PropertyChanges {
565618 target: appDelegate;
566− x: -appDelegate.width / 2;
619+ requestedX: -appDelegate.width / 2;
567620 scale: units.gu(5) / appDelegate.width;
568621 opacity: 0;
569622 visuallyMinimized: true;
@@ -573,21 +626,17 @@ AbstractStage {
573626 ]
574627 transitions: [
575628 Transition {
576− to: "normal"
629+ to: "normal,restored"
577630 enabled: appDelegate.animationsEnabled
578631 PropertyAction { target: appDelegate; properties: "visuallyMinimized,visuallyMaximized" }
579− UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,requestedWidth,requestedHeight,scale"; duration: UbuntuAnimation.FastDuration }
580− UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
632+ UbuntuNumberAnimation { target: appDelegate; properties: "requestedX,requestedY,opacity,scale,requestedWidth,requestedHeight" }
581633 },
582634 Transition {
583635 to: "minimized"
584636 enabled: appDelegate.animationsEnabled
585637 PropertyAction { target: appDelegate; property: "visuallyMaximized" }
586638 SequentialAnimation {
587− ParallelAnimation {
588− UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,scale"; duration: UbuntuAnimation.FastDuration }
589− UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
590− }
639+ UbuntuNumberAnimation { target: appDelegate; properties: "requestedX,requestedY,opacity,scale,requestedWidth,requestedHeight" }
591640 PropertyAction { target: appDelegate; property: "visuallyMinimized" }
592641 ScriptAction {
593642 script: {
@@ -604,10 +653,8 @@ AbstractStage {
604653 enabled: appDelegate.animationsEnabled
605654 PropertyAction { target: appDelegate; property: "visuallyMinimized" }
606655 SequentialAnimation {
607− ParallelAnimation {
608− UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,scale"; duration: UbuntuAnimation.FastDuration }
609− UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
610− }
656+ ScriptAction { script: { fakeRectangle.stop(); } }
657+ UbuntuNumberAnimation { target: appDelegate; properties: "requestedX,requestedY,opacity,scale,requestedWidth,requestedHeight" }
611658 PropertyAction { target: appDelegate; property: "visuallyMaximized" }
612659 }
613660 }
@@ -674,27 +721,45 @@ AbstractStage {
674721 surface: model.surface
675722 active: appDelegate.focus
676723 focus: true
677− maximizeButtonShown: (maximumWidth == 0 || maximumWidth >= appContainer.width) &&
678− (maximumHeight == 0 || maximumHeight >= appContainer.height)
724+ maximizeButtonShown: appDelegate.canBeMaximized
679725 overlayShown: touchControls.overlayShown
726+ stageWidth: appContainer.width
727+ stageHeight: appContainer.height
680728
681729 requestedWidth: appDelegate.requestedWidth
682730 requestedHeight: appDelegate.requestedHeight
683731
684732 onCloseClicked: { appDelegate.close(); }
685− onMaximizeClicked: appDelegate.maximized || appDelegate.maximizedLeft || appDelegate.maximizedRight
686− || appDelegate.maximizedHorizontally || appDelegate.maximizedVertically
687− ? appDelegate.restoreFromMaximized() : appDelegate.maximize()
733+ onMaximizeClicked: appDelegate.anyMaximized ? appDelegate.restoreFromMaximized() : appDelegate.maximize();
688734 onMaximizeHorizontallyClicked: appDelegate.maximizedHorizontally ? appDelegate.restoreFromMaximized() : appDelegate.maximizeHorizontally()
689735 onMaximizeVerticallyClicked: appDelegate.maximizedVertically ? appDelegate.restoreFromMaximized() : appDelegate.maximizeVertically()
690736 onMinimizeClicked: appDelegate.minimize()
691737 onDecorationPressed: { appDelegate.focus = true; }
738+
739+ onFakeMaximizeAnimationRequested: if (!appDelegate.maximized) fakeRectangle.maximize(progress)
740+ onFakeMaximizeLeftAnimationRequested: if (!appDelegate.maximizedLeft) fakeRectangle.maximizeLeft(progress)
741+ onFakeMaximizeRightAnimationRequested: if (!appDelegate.maximizedRight) fakeRectangle.maximizeRight(progress)
742+ onFakeMaximizeTopLeftAnimationRequested: if (!appDelegate.maximizedTopLeft) fakeRectangle.maximizeTopLeft(progress);
743+ onFakeMaximizeTopRightAnimationRequested: if (!appDelegate.maximizedTopRight) fakeRectangle.maximizeTopRight(progress);
744+ onFakeMaximizeBottomLeftAnimationRequested: if (!appDelegate.maximizedBottomLeft) fakeRectangle.maximizeBottomLeft(progress);
745+ onFakeMaximizeBottomRightAnimationRequested: if (!appDelegate.maximizedBottomRight) fakeRectangle.maximizeBottomRight(progress);
746+ onStopFakeAnimation: fakeRectangle.stop();
692747 }
693748
694749 WindowControlsOverlay {
695750 id: touchControls
696− anchors.fill: appDelegate
697751 target: appDelegate
752+ stageWidth: appContainer.width
753+ stageHeight: appContainer.height
754+
755+ onFakeMaximizeAnimationRequested: decoratedWindow.fakeMaximizeAnimationRequested(progress)
756+ onFakeMaximizeLeftAnimationRequested: decoratedWindow.fakeMaximizeLeftAnimationRequested(progress)
757+ onFakeMaximizeRightAnimationRequested: decoratedWindow.fakeMaximizeRightAnimationRequested(progress)
758+ onFakeMaximizeTopLeftAnimationRequested: decoratedWindow.fakeMaximizeTopLeftAnimationRequested(progress)
759+ onFakeMaximizeTopRightAnimationRequested: decoratedWindow.fakeMaximizeTopRightAnimationRequested(progress)
760+ onFakeMaximizeBottomLeftAnimationRequested: decoratedWindow.fakeMaximizeBottomLeftAnimationRequested(progress)
761+ onFakeMaximizeBottomRightAnimationRequested: decoratedWindow.fakeMaximizeBottomRightAnimationRequested(progress)
762+ onStopFakeAnimation: decoratedWindow.stopFakeAnimation()
698763 }
699764
700765 WindowedFullscreenPolicy {
@@ -706,6 +771,14 @@ AbstractStage {
706771 }
707772 }
708773
774+ FakeMaximizeDelegate {
775+ id: fakeRectangle
776+ target: priv.focusedAppDelegate
777+ leftMargin: root.leftMargin
778+ appContainerWidth: appContainer.width
779+ appContainerHeight: appContainer.height
780+ }
781+
709782 EdgeBarrier {
710783 id: edgeBarrier
711784
qml/Stages/FakeMaximizeDelegate.qmlnew file+186
@@ -0,0 +1,186 @@
1+/*
2+ * Copyright (C) 2016 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.4
18+import Ubuntu.Components 1.3
19+import "../Components/PanelState"
20+
21+Rectangle {
22+ id: fakeRectangle
23+ visible: opacity > 0 // we go from 0.2 to 0.6
24+ enabled: visible
25+ color: "#ffffff"
26+ border.width: units.dp(1)
27+ border.color: "#99ffffff"
28+ opacity: 0
29+
30+ property int edge: -1 // Item.TransformOrigin
31+ property var target // appDelegate
32+ property int leftMargin
33+ property real appContainerWidth
34+ property real appContainerHeight
35+
36+ QtObject {
37+ id: priv
38+
39+ property real progress: 0
40+
41+ function setup(edge) {
42+ fakeRectangle.x = target.x;
43+ fakeRectangle.y = target.y;
44+ fakeRectangle.width = target.width;
45+ fakeRectangle.height = target.height;
46+ fakeRectangle.edge = edge;
47+ fakeRectangle.transformOrigin = edge;
48+ }
49+
50+ function projectOpacity(progress) {
51+ // progress falls into [0,1], opacity moves in [0.2,0.6]
52+ fakeRectangle.opacity = MathUtils.clampAndProject(progress, 0.0, 1.0, 0.2, 0.6);
53+ }
54+
55+ function projectScale(progress) {
56+ // progress falls into [0,1], scale moves in [1,2] as a hint, when under 30%
57+ fakeRectangle.scale = MathUtils.clampAndProject(progress, 0, 1, 1, 2);
58+ }
59+
60+ function processAnimation(progress, animation) {
61+ if (priv.progress >= 0.3) {
62+ fakeRectangle.scale = 1;
63+ animation.start();
64+ } else if (progress > priv.progress) {
65+ priv.progress += progress;
66+ priv.projectScale(progress);
67+ }
68+ priv.projectOpacity(progress);
69+ }
70+ }
71+
72+ function stop() {
73+ print("!!! Stop")
74+ opacity = 0;
75+ edge = -1;
76+ priv.progress = 0;
77+ }
78+
79+ function maximize(progress) {
80+ if (fakeRectangle.edge != Item.Top) {
81+ priv.setup(Item.Top);
82+ }
83+ priv.processAnimation(progress, fakeMaximizeAnimation);
84+ }
85+
86+ function maximizeLeft(progress) {
87+ if (fakeRectangle.edge != Item.Left) {
88+ priv.setup(Item.Left);
89+ }
90+ priv.processAnimation(progress, fakeMaximizeLeftAnimation);
91+ }
92+
93+ function maximizeRight(progress) {
94+ if (fakeRectangle.edge != Item.Right) {
95+ priv.setup(Item.Right);
96+ }
97+ priv.processAnimation(progress, fakeMaximizeRightAnimation);
98+ }
99+
100+ function maximizeTopLeft(progress) {
101+ if (fakeRectangle.edge != Item.TopLeft) {
102+ priv.setup(Item.TopLeft);
103+ }
104+ priv.processAnimation(progress, fakeMaximizeTopLeftAnimation);
105+ }
106+
107+ function maximizeTopRight(progress) {
108+ if (fakeRectangle.edge != Item.TopRight) {
109+ priv.setup(Item.TopRight);
110+ }
111+ priv.processAnimation(progress, fakeMaximizeTopRightAnimation);
112+ }
113+
114+ function maximizeBottomLeft(progress) {
115+ if (fakeRectangle.edge != Item.BottomLeft) {
116+ priv.setup(Item.BottomLeft);
117+ }
118+ priv.processAnimation(progress, fakeMaximizeBottomLeftAnimation);
119+ }
120+
121+ function maximizeBottomRight(progress) {
122+ if (fakeRectangle.edge != Item.BottomRight) {
123+ priv.setup(Item.BottomRight);
124+ }
125+ priv.processAnimation(progress, fakeMaximizeBottomRightAnimation);
126+ }
127+
128+ Behavior on opacity { UbuntuNumberAnimation { duration: UbuntuAnimation.BriskDuration } }
129+ Behavior on scale { UbuntuNumberAnimation { duration: UbuntuAnimation.BriskDuration } }
130+
131+ ParallelAnimation {
132+ id: fakeMaximizeAnimation
133+ UbuntuNumberAnimation { target: fakeRectangle; properties: "x"; duration: UbuntuAnimation.BriskDuration; to: leftMargin }
134+ UbuntuNumberAnimation { target: fakeRectangle; properties: "y"; duration: UbuntuAnimation.BriskDuration; to: PanelState.panelHeight }
135+ UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: appContainerWidth - leftMargin }
136+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: appContainerHeight }
137+ }
138+
139+ ParallelAnimation {
140+ id: fakeMaximizeLeftAnimation
141+ UbuntuNumberAnimation { target: fakeRectangle; properties: "x"; duration: UbuntuAnimation.BriskDuration; to: leftMargin }
142+ UbuntuNumberAnimation { target: fakeRectangle; properties: "y"; duration: UbuntuAnimation.BriskDuration; to: PanelState.panelHeight }
143+ UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth - leftMargin)/2 }
144+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: appContainerHeight - PanelState.panelHeight }
145+ }
146+
147+ ParallelAnimation {
148+ id: fakeMaximizeRightAnimation
149+ UbuntuNumberAnimation { target: fakeRectangle; properties: "x"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth + leftMargin)/2 }
150+ UbuntuNumberAnimation { target: fakeRectangle; properties: "y"; duration: UbuntuAnimation.BriskDuration; to: PanelState.panelHeight }
151+ UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth - leftMargin)/2 }
152+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: appContainerHeight - PanelState.panelHeight }
153+ }
154+
155+ ParallelAnimation {
156+ id: fakeMaximizeTopLeftAnimation
157+ UbuntuNumberAnimation { target: fakeRectangle; properties: "x"; duration: UbuntuAnimation.BriskDuration; to: leftMargin }
158+ UbuntuNumberAnimation { target: fakeRectangle; properties: "y"; duration: UbuntuAnimation.BriskDuration; to: PanelState.panelHeight }
159+ UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth - leftMargin)/2 }
160+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: (appContainerHeight - PanelState.panelHeight)/2 }
161+ }
162+
163+ ParallelAnimation {
164+ id: fakeMaximizeTopRightAnimation
165+ UbuntuNumberAnimation { target: fakeRectangle; properties: "x"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth + leftMargin)/2 }
166+ UbuntuNumberAnimation { target: fakeRectangle; properties: "y"; duration: UbuntuAnimation.BriskDuration; to: PanelState.panelHeight }
167+ UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth - leftMargin)/2 }
168+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: (appContainerHeight - PanelState.panelHeight)/2 }
169+ }
170+
171+ ParallelAnimation {
172+ id: fakeMaximizeBottomLeftAnimation
173+ UbuntuNumberAnimation { target: fakeRectangle; properties: "x"; duration: UbuntuAnimation.BriskDuration; to: leftMargin }
174+ UbuntuNumberAnimation { target: fakeRectangle; properties: "y"; duration: UbuntuAnimation.BriskDuration; to: (appContainerHeight + PanelState.panelHeight)/2 }
175+ UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth - leftMargin)/2 }
176+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: appContainerHeight/2 }
177+ }
178+
179+ ParallelAnimation {
180+ id: fakeMaximizeBottomRightAnimation
181+ UbuntuNumberAnimation { target: fakeRectangle; properties: "x"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth + leftMargin)/2 }
182+ UbuntuNumberAnimation { target: fakeRectangle; properties: "y"; duration: UbuntuAnimation.BriskDuration; to: (appContainerHeight + PanelState.panelHeight)/2 }
183+ UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth - leftMargin)/2 }
184+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: appContainerHeight/2 }
185+ }
186+}
qml/Stages/WindowControlsOverlay.qml+158−16
@@ -23,13 +23,25 @@ import "../Components/PanelState"
2323Item {
2424 id: root
2525 enabled: target && !target.fullscreen
26+ anchors.fill: target
2627
2728 // to be set from outside
2829 property Item target // appDelegate
30+ property int stageWidth
31+ property int stageHeight
2932
3033 // to be read from outside
3134 readonly property alias overlayShown: overlay.visible
3235
36+ signal fakeMaximizeAnimationRequested(real progress)
37+ signal fakeMaximizeLeftAnimationRequested(real progress)
38+ signal fakeMaximizeRightAnimationRequested(real progress)
39+ signal fakeMaximizeTopLeftAnimationRequested(real progress)
40+ signal fakeMaximizeTopRightAnimationRequested(real progress)
41+ signal fakeMaximizeBottomLeftAnimationRequested(real progress)
42+ signal fakeMaximizeBottomRightAnimationRequested(real progress)
43+ signal stopFakeAnimation()
44+
3345 TouchGestureArea {
3446 id: gestureArea
3547 anchors.fill: parent
@@ -79,6 +91,26 @@ Item {
7991 property bool dragging
8092
8193 readonly property var resizeArea: root.target && root.target.resizeArea ? root.target.resizeArea : null
94+ readonly property bool ensureWindow: root.target.state == "normal" || root.target.state == "restored"
95+
96+ readonly property int triggerArea: units.gu(8)
97+ property bool nearLeftEdge: target.maximizedLeft
98+ property bool nearTopEdge: target.maximized
99+ property bool nearRightEdge: target.maximizedRight
100+ property bool nearTopLeftCorner: target.maximizedTopLeft
101+ property bool nearTopRightCorner: target.maximizedTopRight
102+ property bool nearBottomLeftCorner: target.maximizedBottomLeft
103+ property bool nearBottomRightCorner: target.maximizedBottomRight
104+
105+ function resetEdges() {
106+ nearLeftEdge = false;
107+ nearRightEdge = false;
108+ nearTopEdge = false;
109+ nearTopLeftCorner = false;
110+ nearTopRightCorner = false;
111+ nearBottomLeftCorner = false;
112+ nearBottomRightCorner = false;
113+ }
82114
83115 function handlePressedChanged(pressed, mouseX, mouseY) {
84116 if (pressed) {
@@ -92,12 +124,31 @@ Item {
92124 }
93125
94126 function handlePositionChanged(mouseX, mouseY) {
95− if (priv.dragging) {
96− var pos = mapToItem(root.target.parent, mouseX, mouseY);
97− root.target.requestedX = Math.round(pos.x - priv.distanceX);
98− root.target.requestedY = Math.round(Math.max(pos.y - priv.distanceY, PanelState.panelHeight));
99− }
127+ var pos = mapToItem(root.target.parent, mouseX, mouseY);
128+ root.target.requestedX = Math.round(pos.x - priv.distanceX);
129+ root.target.requestedY = Math.round(Math.max(pos.y - priv.distanceY, PanelState.panelHeight));
130+ }
131+
132+ function saveRestoredPos(x, y) {
133+ var pos = mapToItem(root.target, x, y);
134+ target.restoredX = Math.round(pos.x);
135+ target.restoredY = Math.round(pos.y);
136+ }
137+
138+ // return the progress of mouse pointer movement from 0 to 1 within a corner square of the screen
139+ // 0 -> before the mouse enters the square
140+ // 1 -> mouse is exactly in the very corner
141+ // a is the corner, b is the mouse pos
142+ function progressInCorner(ax, ay, bx, by) {
143+ // distance of two points, a and b, in pixels
144+ var distance = Math.sqrt(Math.pow(bx-ax, 2) + Math.pow(by-ay, 2));
145+ // length of the triggerArea square diagonal
146+ var diagLength = Math.sqrt(2 * priv.triggerArea * priv.triggerArea);
147+ var ratio = 1 - (distance / diagLength);
148+ return bx > 0 && bx <= stageWidth && by > 0 && by <= stageHeight ? ratio : 1; // everything "outside" of our square from the center is 1
100149 }
150+
151+ property real progress: 0
101152 }
102153
103154 // the visual overlay
@@ -113,8 +164,6 @@ Item {
113164 UbuntuNumberAnimation {}
114165 }
115166
116− readonly property bool anyMaximized: target && (target.maximized || target.maximizedLeft || target.maximizedRight)
117−
118167 Image {
119168 source: "graphics/arrows-centre.png"
120169 width: units.gu(10)
@@ -133,7 +182,98 @@ Item {
133182 cursorShape: priv.dragging ? Qt.ClosedHandCursor : (overlay.visible ? Qt.OpenHandCursor : Qt.ArrowCursor)
134183
135184 onPressedChanged: priv.handlePressedChanged(pressed, mouseX, mouseY)
136− onPositionChanged: priv.handlePositionChanged(mouseX, mouseY)
185+ onPositionChanged: {
186+ if (priv.dragging) {
187+ priv.handlePositionChanged(mouseX, mouseY);
188+
189+ var globalPos = mapToItem(null, mouseX, mouseY);
190+ var globalX = globalPos.x;
191+ var globalY = globalPos.y;
192+ if (globalX < priv.triggerArea && globalY < PanelState.panelHeight) { // top left
193+ if (target.canBeCornerMaximized) {
194+ priv.progress = priv.progressInCorner(0, PanelState.panelHeight, globalX, globalY);
195+ priv.resetEdges();
196+ priv.nearTopLeftCorner = true;
197+ root.fakeMaximizeTopLeftAnimationRequested(priv.progress);
198+ }
199+ } else if (globalX > stageWidth - priv.triggerArea && globalY < PanelState.panelHeight) { // top right
200+ if (target.canBeCornerMaximized) {
201+ priv.progress = priv.progressInCorner(stageWidth, PanelState.panelHeight, globalX, globalY);
202+ priv.resetEdges();
203+ priv.nearTopRightCorner = true;
204+ root.fakeMaximizeTopRightAnimationRequested(priv.progress);
205+ }
206+ } else if (globalX < priv.triggerArea && globalY > stageHeight - priv.triggerArea) { // bottom left
207+ if (target.canBeCornerMaximized) {
208+ priv.progress = priv.progressInCorner(0, stageHeight, globalX, globalY);
209+ priv.resetEdges();
210+ priv.nearBottomLeftCorner = true;
211+ root.fakeMaximizeBottomLeftAnimationRequested(priv.progress);
212+ }
213+ } else if (globalX > stageWidth - priv.triggerArea && globalY > stageHeight - priv.triggerArea) { // bottom right
214+ if (target.canBeCornerMaximized) {
215+ priv.progress = priv.progressInCorner(stageWidth, stageHeight, globalX, globalY);
216+ priv.resetEdges();
217+ priv.nearBottomRightCorner = true;
218+ root.fakeMaximizeBottomRightAnimationRequested(priv.progress);
219+ }
220+ } else if (globalX < priv.triggerArea) { // left
221+ if (target.canBeMaximizedLeftRight) {
222+ priv.progress = MathUtils.clampAndProject(globalX, priv.triggerArea, 0, 0, 1);
223+ priv.resetEdges();
224+ priv.nearLeftEdge = true;
225+ root.fakeMaximizeLeftAnimationRequested(priv.progress);
226+ }
227+ } else if (globalX > stageWidth - priv.triggerArea) { // right
228+ if (target.canBeMaximizedLeftRight) {
229+ priv.progress = MathUtils.clampAndProject(globalX, stageWidth - priv.triggerArea, stageWidth, 0, 1);
230+ priv.resetEdges();
231+ priv.nearRightEdge = true;
232+ root.fakeMaximizeRightAnimationRequested(priv.progress);
233+ }
234+ } else if (globalY < PanelState.panelHeight) { // top
235+ if (target.canBeMaximized) {
236+ priv.progress = MathUtils.clampAndProject(globalY, Math.max(PanelState.panelHeight, priv.triggerArea), 0, 0, 1);
237+ priv.resetEdges();
238+ priv.nearTopEdge = true;
239+ root.fakeMaximizeAnimationRequested(priv.progress);
240+ }
241+ } else if (priv.nearLeftEdge || priv.nearRightEdge || priv.nearTopEdge || priv.nearTopLeftCorner || priv.nearTopRightCorner ||
242+ priv.nearBottomLeftCorner || priv.nearBottomRightCorner) {
243+ print("!!! Exited")
244+ priv.progress = 0;
245+ priv.resetEdges();
246+ root.stopFakeAnimation();
247+ } else if (target.anyMaximized) {
248+ priv.progress = 0;
249+ target.restoreFromMaximized();
250+ }
251+ }
252+ }
253+ onReleased: {
254+ print("Mouse released (left/top/right)", priv.nearLeftEdge, priv.nearTopEdge, priv.nearRightEdge)
255+ if (mouse.button == Qt.LeftButton && (target.state == "normal" || target.state == "restored") && priv.progress == 0) {
256+ priv.saveRestoredPos(target.x, target.y);
257+ } else if (priv.progress < 0.3) { // cancel the preview shape if under 30%
258+ priv.progress = 0;
259+ priv.resetEdges();
260+ root.stopFakeAnimation();
261+ } else if (priv.nearLeftEdge) {
262+ target.maximizeLeft();
263+ } else if (priv.nearTopEdge) {
264+ target.maximize();
265+ } else if (priv.nearRightEdge) {
266+ target.maximizeRight();
267+ } else if (priv.nearTopLeftCorner) {
268+ target.maximizeTopLeft();
269+ } else if (priv.nearTopRightCorner) {
270+ target.maximizeTopRight();
271+ } else if (priv.nearBottomLeftCorner) {
272+ target.maximizeBottomLeft();
273+ } else if (priv.nearBottomRightCorner) {
274+ target.maximizeBottomRight();
275+ }
276+ }
137277 }
138278
139279 // dismiss area
@@ -157,7 +297,7 @@ Item {
157297 ResizeGrip { // top left
158298 anchors.horizontalCenter: parent.left
159299 anchors.verticalCenter: parent.top
160− visible: target && !overlay.anyMaximized && !target.maximizedHorizontally && !target.maximizedVertically
300+ visible: priv.ensureWindow || target.maximizedBottomRight
161301 resizeTarget: priv.resizeArea
162302 }
163303
@@ -165,7 +305,7 @@ Item {
165305 anchors.horizontalCenter: parent.horizontalCenter
166306 anchors.verticalCenter: parent.top
167307 rotation: 45
168− visible: target && !overlay.anyMaximized && !target.maximizedVertically
308+ visible: priv.ensureWindow || target.maximizedHorizontally || target.maximizedBottomLeft || target.maximizedBottomRight
169309 resizeTarget: priv.resizeArea
170310 }
171311
@@ -173,7 +313,7 @@ Item {
173313 anchors.horizontalCenter: parent.right
174314 anchors.verticalCenter: parent.top
175315 rotation: 90
176− visible: target && !overlay.anyMaximized && !target.maximizedHorizontally && !target.maximizedVertically
316+ visible: priv.ensureWindow || target.maximizedBottomLeft
177317 resizeTarget: priv.resizeArea
178318 }
179319
@@ -181,14 +321,15 @@ Item {
181321 anchors.horizontalCenter: parent.right
182322 anchors.verticalCenter: parent.verticalCenter
183323 rotation: 135
184− visible: target && !target.maximizedRight && !target.maximized && !target.maximizedHorizontally
324+ visible: priv.ensureWindow || target.maximizedVertically || target.maximizedLeft ||
325+ target.maximizedTopLeft || target.maximizedBottomLeft
185326 resizeTarget: priv.resizeArea
186327 }
187328
188329 ResizeGrip { // bottom right
189330 anchors.horizontalCenter: parent.right
190331 anchors.verticalCenter: parent.bottom
191− visible: target && !overlay.anyMaximized && !target.maximizedHorizontally && !target.maximizedVertically
332+ visible: priv.ensureWindow || target.maximizedTopLeft
192333 resizeTarget: priv.resizeArea
193334 }
194335
@@ -196,7 +337,7 @@ Item {
196337 anchors.horizontalCenter: parent.horizontalCenter
197338 anchors.verticalCenter: parent.bottom
198339 rotation: 45
199− visible: target && !overlay.anyMaximized && !target.maximizedVertically
340+ visible: priv.ensureWindow || target.maximizedHorizontally || target.maximizedTopLeft || target.maximizedTopRight
200341 resizeTarget: priv.resizeArea
201342 }
202343
@@ -204,7 +345,7 @@ Item {
204345 anchors.horizontalCenter: parent.left
205346 anchors.verticalCenter: parent.bottom
206347 rotation: 90
207− visible: target && !overlay.anyMaximized && !target.maximizedHorizontally && !target.maximizedVertically
348+ visible: priv.ensureWindow || target.maximizedTopRight
208349 resizeTarget: priv.resizeArea
209350 }
210351
@@ -212,7 +353,8 @@ Item {
212353 anchors.horizontalCenter: parent.left
213354 anchors.verticalCenter: parent.verticalCenter
214355 rotation: 135
215− visible: target && !target.maximizedLeft && !target.maximized && !target.maximizedHorizontally
356+ visible: priv.ensureWindow || target.maximizedVertically || target.maximizedRight ||
357+ target.maximizedTopRight || target.maximizedBottomRight
216358 resizeTarget: priv.resizeArea
217359 }
218360 }
qml/Stages/WindowDecoration.qml+150−8
@@ -24,12 +24,16 @@ MouseArea {
2424 id: root
2525 clip: true
2626
27− property Item target
27+ property Item target // appDelegate
2828 property alias title: titleLabel.text
2929 property alias maximizeButtonShown: buttons.maximizeButtonShown
3030 property bool active: false
31− acceptedButtons: Qt.AllButtons // prevent leaking unhandled mouse events
3231 property alias overlayShown: buttons.overlayShown
32+ property int stageWidth
33+ property int stageHeight
34+
35+ acceptedButtons: Qt.AllButtons // prevent leaking unhandled mouse events
36+ hoverEnabled: true
3337
3438 signal closeClicked()
3539 signal minimizeClicked()
@@ -37,8 +41,18 @@ MouseArea {
3741 signal maximizeHorizontallyClicked()
3842 signal maximizeVerticallyClicked()
3943
44+ signal fakeMaximizeAnimationRequested(real progress)
45+ signal fakeMaximizeLeftAnimationRequested(real progress)
46+ signal fakeMaximizeRightAnimationRequested(real progress)
47+ signal fakeMaximizeTopLeftAnimationRequested(real progress)
48+ signal fakeMaximizeTopRightAnimationRequested(real progress)
49+ signal fakeMaximizeBottomLeftAnimationRequested(real progress)
50+ signal fakeMaximizeBottomRightAnimationRequested(real progress)
51+ signal stopFakeAnimation()
52+
4053 onDoubleClicked: {
41− if (maximizeButtonShown && mouse.button == Qt.LeftButton) {
54+ priv.resetEdges();
55+ if (target.canBeMaximized && mouse.button == Qt.LeftButton) {
4256 root.maximizeClicked();
4357 }
4458 }
@@ -48,6 +62,46 @@ MouseArea {
4862 property real distanceX
4963 property real distanceY
5064 property bool dragging
65+
66+ readonly property int triggerArea: units.gu(3)
67+ property bool nearLeftEdge: target.maximizedLeft
68+ property bool nearTopEdge: target.maximized
69+ property bool nearRightEdge: target.maximizedRight
70+ property bool nearTopLeftCorner: target.maximizedTopLeft
71+ property bool nearTopRightCorner: target.maximizedTopRight
72+ property bool nearBottomLeftCorner: target.maximizedBottomLeft
73+ property bool nearBottomRightCorner: target.maximizedBottomRight
74+
75+ function resetEdges() {
76+ nearLeftEdge = false;
77+ nearRightEdge = false;
78+ nearTopEdge = false;
79+ nearTopLeftCorner = false;
80+ nearTopRightCorner = false;
81+ nearBottomLeftCorner = false;
82+ nearBottomRightCorner = false;
83+ }
84+
85+ function saveRestoredPos(x, y) {
86+ var pos = mapToItem(root.target, x, y);
87+ target.restoredX = Math.round(pos.x);
88+ target.restoredY = Math.round(pos.y);
89+ print("!!! Saved pos", target.restoredX, target.restoredY)
90+ }
91+
92+ // return the progress of mouse pointer movement from 0 to 1 within a corner square of the screen
93+ // 0 -> before the mouse enters the square
94+ // 1 -> mouse is exactly in the very corner
95+ // a is the corner, b is the mouse pos
96+ function progressInCorner(ax, ay, bx, by) {
97+ // distance of two points, a and b, in pixels
98+ var distance = Math.sqrt(Math.pow(bx-ax, 2) + Math.pow(by-ay, 2));
99+ // length of the triggerArea square diagonal
100+ var diagLength = Math.sqrt(2 * priv.triggerArea * priv.triggerArea);
101+ var ratio = 1 - (distance / diagLength);
102+ return bx > 0 && bx <= stageWidth && by > 0 && by <= stageHeight ? ratio : 1; // everything "outside" of our square from the center is 1
103+ }
104+ property real progress: 0
51105 }
52106
53107 onPressedChanged: {
@@ -69,8 +123,97 @@ MouseArea {
69123 // Use integer coordinate values to ensure that target is left in a pixel-aligned
70124 // position. Mouse movement could have subpixel precision, yielding a fractional
71125 // mouse position.
126+
72127 root.target.requestedX = Math.round(pos.x - priv.distanceX);
73128 root.target.requestedY = Math.round(Math.max(pos.y - priv.distanceY, PanelState.panelHeight));
129+
130+ var globalPos = mapToItem(null, mouseX, mouseY);
131+ var globalX = globalPos.x;
132+ var globalY = globalPos.y;
133+ if (globalX < priv.triggerArea && globalY < PanelState.panelHeight) { // top left
134+ if (target.canBeCornerMaximized) {
135+ priv.progress = priv.progressInCorner(0, PanelState.panelHeight, globalX, globalY);
136+ priv.resetEdges();
137+ priv.nearTopLeftCorner = true;
138+ root.fakeMaximizeTopLeftAnimationRequested(priv.progress);
139+ }
140+ } else if (globalX > stageWidth - priv.triggerArea && globalY < PanelState.panelHeight) { // top right
141+ if (target.canBeCornerMaximized) {
142+ priv.progress = priv.progressInCorner(stageWidth, PanelState.panelHeight, globalX, globalY);
143+ priv.resetEdges();
144+ priv.nearTopRightCorner = true;
145+ root.fakeMaximizeTopRightAnimationRequested(priv.progress);
146+ }
147+ } else if (globalX < priv.triggerArea && globalY > stageHeight - priv.triggerArea) { // bottom left
148+ if (target.canBeCornerMaximized) {
149+ priv.progress = priv.progressInCorner(0, stageHeight, globalX, globalY);
150+ priv.resetEdges();
151+ priv.nearBottomLeftCorner = true;
152+ root.fakeMaximizeBottomLeftAnimationRequested(priv.progress);
153+ }
154+ } else if (globalX > stageWidth - priv.triggerArea && globalY > stageHeight - priv.triggerArea) { // bottom right
155+ if (target.canBeCornerMaximized) {
156+ priv.progress = priv.progressInCorner(stageWidth, stageHeight, globalX, globalY);
157+ priv.resetEdges();
158+ priv.nearBottomRightCorner = true;
159+ root.fakeMaximizeBottomRightAnimationRequested(priv.progress);
160+ }
161+ } else if (globalX < priv.triggerArea) { // left
162+ if (target.canBeMaximizedLeftRight) {
163+ priv.progress = MathUtils.clampAndProject(globalX, priv.triggerArea, 0, 0, 1);
164+ priv.resetEdges();
165+ priv.nearLeftEdge = true;
166+ root.fakeMaximizeLeftAnimationRequested(priv.progress);
167+ }
168+ } else if (globalX > stageWidth - priv.triggerArea) { // right
169+ if (target.canBeMaximizedLeftRight) {
170+ priv.progress = MathUtils.clampAndProject(globalX, stageWidth - priv.triggerArea, stageWidth, 0, 1);
171+ priv.resetEdges();
172+ priv.nearRightEdge = true;
173+ root.fakeMaximizeRightAnimationRequested(priv.progress);
174+ }
175+ } else if (globalY < PanelState.panelHeight) { // top
176+ if (target.canBeMaximized) {
177+ priv.progress = MathUtils.clampAndProject(globalY, Math.max(PanelState.panelHeight, priv.triggerArea), 0, 0, 1);
178+ priv.resetEdges();
179+ priv.nearTopEdge = true;
180+ root.fakeMaximizeAnimationRequested(priv.progress);
181+ }
182+ } else if (priv.nearLeftEdge || priv.nearRightEdge || priv.nearTopEdge || priv.nearTopLeftCorner || priv.nearTopRightCorner ||
183+ priv.nearBottomLeftCorner || priv.nearBottomRightCorner) {
184+ print("!!! Exited")
185+ priv.progress = 0;
186+ priv.resetEdges();
187+ root.stopFakeAnimation();
188+ } else if (target.anyMaximized) {
189+ priv.progress = 0;
190+ target.restoreFromMaximized();
191+ }
192+ }
193+ }
194+
195+ onReleased: {
196+ print("Mouse released (left/top/right)", priv.nearLeftEdge, priv.nearTopEdge, priv.nearRightEdge)
197+ if (mouse.button == Qt.LeftButton && (target.state == "normal" || target.state == "restored") && priv.progress == 0) {
198+ priv.saveRestoredPos(target.x, target.y);
199+ } else if (priv.progress < 0.3) { // cancel the preview shape if under 30%
200+ priv.progress = 0;
201+ priv.resetEdges();
202+ root.stopFakeAnimation();
203+ } else if (priv.nearLeftEdge) {
204+ target.maximizeLeft();
205+ } else if (priv.nearTopEdge) {
206+ target.maximize();
207+ } else if (priv.nearRightEdge) {
208+ target.maximizeRight();
209+ } else if (priv.nearTopLeftCorner) {
210+ target.maximizeTopLeft();
211+ } else if (priv.nearTopRightCorner) {
212+ target.maximizeTopRight();
213+ } else if (priv.nearBottomLeftCorner) {
214+ target.maximizeBottomLeft();
215+ } else if (priv.nearBottomRightCorner) {
216+ target.maximizeBottomRight();
74217 }
75218 }
76219
@@ -105,10 +248,9 @@ MouseArea {
105248 onCloseClicked: root.closeClicked();
106249 onMinimizeClicked: root.minimizeClicked();
107250 onMaximizeClicked: root.maximizeClicked();
108− onMaximizeHorizontallyClicked: root.maximizeHorizontallyClicked();
109− onMaximizeVerticallyClicked: root.maximizeVerticallyClicked();
251+ onMaximizeHorizontallyClicked: if (root.target.canBeMaximizedHorizontally) root.maximizeHorizontallyClicked();
252+ onMaximizeVerticallyClicked: if (root.target.canBeMaximizedVertically) root.maximizeVerticallyClicked();
110253 closeButtonShown: root.target.application.appId !== "unity8-dash"
111− target: root.target
112254 }
113255
114256 Label {
@@ -122,9 +264,9 @@ MouseArea {
122264 font.weight: root.active ? Font.Light : Font.Medium
123265 elide: Text.ElideRight
124266 opacity: overlayShown ? 0 : 1
125− visible: opacity == 1
267+ visible: opacity != 0
126268 Behavior on opacity {
127− OpacityAnimator { duration: UbuntuAnimation.FastDuration; easing: UbuntuAnimation.StandardEasing }
269+ UbuntuNumberAnimation {}
128270 }
129271 }
130272 }
qml/Stages/WindowResizeArea.qml+11−24
@@ -54,7 +54,7 @@ MouseArea {
5454 property int normalHeight: 0
5555
5656 function updateNormalGeometry() {
57− if (root.target.state == "normal") {
57+ if (root.target.state == "normal" || root.target.state == "restored") {
5858 normalX = root.target.requestedX
5959 normalY = root.target.requestedY
6060 normalWidth = root.target.width
@@ -83,34 +83,21 @@ MouseArea {
8383 target.requestedY = Qt.binding(function() { return Math.max(Math.min(windowGeometry.y, root.screenHeight - target.requestedHeight), PanelState.panelHeight); });
8484
8585 var windowState = windowStateStorage.getState(root.windowId, WindowStateStorage.WindowStateNormal)
86− switch (windowState) {
87− case WindowStateStorage.WindowStateNormal:
88− break;
89− case WindowStateStorage.WindowStateMaximized:
90− target.maximize(false);
91− break;
92− case WindowStateStorage.WindowStateMaximizedLeft:
93− target.maximizeLeft(false);
94− break;
95− case WindowStateStorage.WindowStateMaximizedRight:
96− target.maximizeRight(false);
97− break;
98− case WindowStateStorage.WindowStateMaximizedHorizontally:
99− target.maximizeHorizontally(false);
100− break;
101− case WindowStateStorage.WindowStateMaximizedVertically:
102− target.maximizeVertically(false);
103− break;
104− default:
105− console.warn("Unsupported window state");
106− break;
107− }
86+ target.restore(false /* animated */, windowState);
10887
10988 priv.updateNormalGeometry();
89+
90+ // initialize the x/y to restore to
91+ target.restoredX = priv.normalX;
92+ target.restoredY = priv.normalY;
11093 }
11194
11295 function saveWindowState() {
113− windowStateStorage.saveState(root.windowId, target.windowState & ~WindowStateStorage.WindowStateMinimized); // clear the minimized bit when saving
96+ var state = target.windowState;
97+ if (state === WindowStateStorage.WindowStateRestored) {
98+ state = WindowStateStorage.WindowStateNormal;
99+ }
100+ windowStateStorage.saveState(root.windowId, state & ~WindowStateStorage.WindowStateMinimized); // clear the minimized bit when saving
114101 windowStateStorage.saveGeometry(root.windowId, Qt.rect(priv.normalX, priv.normalY, priv.normalWidth, priv.normalHeight));
115102 }
116103
tests/mocks/Utils/windowstatestorage.cpp+2−2
@@ -41,7 +41,7 @@ void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect
4141 m_geometry[windowId] = rect;
4242}
4343
44−QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue)
44+QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue) const
4545{
4646 if (!m_geometry.contains(windowId)) return defaultValue;
4747 return m_geometry.value(windowId).toRect();
@@ -70,7 +70,7 @@ void WindowStateStorage::saveState(const QString &windowId, WindowState state)
7070 m_state[windowId] = state;
7171}
7272
73−WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue)
73+WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue) const
7474{
7575 if (!m_state.contains(windowId)) return defaultValue;
7676 return m_state.value(windowId);
tests/mocks/Utils/windowstatestorage.h+8−3
@@ -32,7 +32,12 @@ public:
3232 WindowStateMaximizedLeft = 1 << 4,
3333 WindowStateMaximizedRight = 1 << 5,
3434 WindowStateMaximizedHorizontally = 1 << 6,
35− WindowStateMaximizedVertically = 1 << 7
35+ WindowStateMaximizedVertically = 1 << 7,
36+ WindowStateMaximizedTopLeft = 1 << 8,
37+ WindowStateMaximizedTopRight = 1 << 9,
38+ WindowStateMaximizedBottomLeft = 1 << 10,
39+ WindowStateMaximizedBottomRight = 1 << 11,
40+ WindowStateRestored = 1 << 12
3641 };
3742 Q_DECLARE_FLAGS(WindowStates, WindowState)
3843#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
@@ -42,10 +47,10 @@ public:
4247 WindowStateStorage(QObject *parent = 0);
4348
4449 Q_INVOKABLE void saveState(const QString &windowId, WindowState state);
45− Q_INVOKABLE WindowState getState(const QString &windowId, WindowState defaultValue);
50+ Q_INVOKABLE WindowState getState(const QString &windowId, WindowState defaultValue) const;
4651
4752 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
48− Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
53+ Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue) const;
4954
5055 Q_INVOKABLE void saveStage(const QString &appId, int stage);
5156 Q_INVOKABLE int getStage(const QString &appId, int defaultValue) const;
tests/qmltests/Stages/tst_DesktopStage.qml+2−2
@@ -487,7 +487,7 @@ Item {
487487
488488 // click again to restore
489489 mouseClick(dialerMaximizeButton);
490− tryCompare(dialerDelegate, "windowState", WindowStateStorage.WindowStateNormal);
490+ tryCompare(dialerDelegate, "windowState", WindowStateStorage.WindowStateRestored);
491491 }
492492
493493 function test_windowMaximizeVertically() {
@@ -502,7 +502,7 @@ Item {
502502
503503 // click again to restore
504504 mouseClick(dialerMaximizeButton);
505− tryCompare(dialerDelegate, "windowState", WindowStateStorage.WindowStateNormal);
505+ tryCompare(dialerDelegate, "windowState", WindowStateStorage.WindowStateRestored);
506506 }
507507
508508 function test_smashCursorKeys() {
tests/qmltests/Stages/tst_WindowResizeArea.qml+9−1
@@ -60,9 +60,12 @@ Item {
6060 property real heightIncrement: 0
6161
6262 property int windowState: WindowStateStorage.WindowStateNormal
63+ property real restoredX
64+ property real restoredY
6365
6466 states: [
6567 State { name: "normal"; when: windowState == WindowStateStorage.WindowStateNormal },
68+ State { name: "restored"; when: windowState == WindowStateStorage.WindowStateRestored },
6669 State { name: "maximized"; when: windowState == WindowStateStorage.WindowStateMaximized }
6770 ]
6871
@@ -71,7 +74,12 @@ Item {
7174 }
7275
7376 function restoreFromMaximized() {
74− windowState = WindowStateStorage.WindowStateNormal
77+ windowState = WindowStateStorage.WindowStateRestored
78+ }
79+
80+ function restore(animated,state) {
81+ windowState = state || WindowStateStorage.WindowStateRestored;
82+ windowState &= ~WindowStateStorage.WindowStateMinimized; // clear the minimized bit
7583 }
7684
7785 WindowResizeArea {