on xenial help sign in
Implement edge maximizing (aka window snapping) (LP: #1602628, #1608465, #1611859)

Approved by: Daniel d'Andrada

commitfc5ef5e7da8095b4d02fd586e38dc5867e4b1348 authorLukáš Tinkl <lukas.tinkl@canonical.com> dateFri 9 Sep 2016, 14:51 (10 years ago) parent999ef197a0df34
25 files changed+835−222
plugins/Cursor/CMakeLists.txt+2−2 plugins/Cursor/MousePointer.cpp+50−14 plugins/Cursor/MousePointer.h+15−2 plugins/Ubuntu/Gestures/TouchGestureArea.h+2−2 plugins/Utils/windowstatestorage.cpp+5−4 plugins/Utils/windowstatestorage.h+8−3 qml/Components/PanelState/PanelState.qml−1 qml/Panel/Panel.qml+6−9 qml/Shell.qml+41 qml/Stages/DecoratedWindow.qml+18 qml/Stages/DesktopStage.qml+139−69 qml/Stages/FakeMaximizeDelegate.qml+233 qml/Stages/MoveHandler.qml+179 qml/Stages/TopLevelSurfaceRepeater.qml+7−1 qml/Stages/WindowControlsOverlay.qml+76−38 qml/Stages/WindowDecoration.qml+10−38 qml/Stages/WindowResizeArea.qml+13−24 tests/mocks/Cursor/CMakeLists.txt+1−1 tests/mocks/Cursor/Cursor.qml+8 tests/mocks/Utils/windowstatestorage.cpp+2−2 tests/mocks/Utils/windowstatestorage.h+8−3 tests/qmltests/Panel/tst_Panel.qml−2 tests/qmltests/Stages/tst_DesktopStage.qml+2−2 tests/qmltests/Stages/tst_WindowResizeArea.qml+9−1 tests/qmltests/tst_Shell.qml+1−4
plugins/Cursor/CMakeLists.txt+2−2
@@ -28,6 +28,6 @@ target_link_libraries(Cursor-qml
2828 ${QT5PLATFORM_SUPPORT_LDFLAGS}
2929)
3030
31−qt5_use_modules(Cursor-qml Qml Quick DBus Network Gui Sql Concurrent Svg)
31+qt5_use_modules(Cursor-qml Qml Quick Svg)
3232
33−add_unity8_plugin(Cursor 1.0 Cursor TARGETS Cursor-qml)
33+add_unity8_plugin(Cursor 1.1 Cursor TARGETS Cursor-qml)
plugins/Cursor/MousePointer.cpp+50−14
@@ -22,6 +22,7 @@
2222
2323#include <QQuickWindow>
2424#include <QGuiApplication>
25+#include <QtMath>
2526
2627#include <qpa/qwindowsysteminterface.h>
2728
@@ -49,25 +50,46 @@ void MousePointer::handleMouseEvent(ulong timestamp, QPointF movement, Qt::Mouse
4950 m_accumulatedMovement -= appliedMovement;
5051
5152 qreal newX = x() + appliedMovement.x();
52− if (newX < 0) {
53− Q_EMIT pushedLeftBoundary(qAbs(newX), buttons);
54− newX = 0;
55− } else if (newX >= parentItem()->width()) {
56− Q_EMIT pushedRightBoundary(newX - (parentItem()->width() - 1), buttons);
57− newX = parentItem()->width() - 1;
58− }
59−
6053 qreal newY = y() + appliedMovement.y();
61− if (newY < 0) {
62− newY = 0;
63− } else if (newY >= parentItem()->height()) {
64− newY = parentItem()->height() - 1;
54+ const qreal sceneWidth = parentItem()->width();
55+ const qreal sceneHeight = parentItem()->height();
56+
57+ if (newX <= 0 && newY < m_topBoundaryOffset) { // top left corner
58+ const auto distance = qSqrt(qPow(newX, 2) + qPow(newY-m_topBoundaryOffset, 2));
59+ Q_EMIT pushedTopLeftCorner(qAbs(distance), buttons);
60+ m_pushing = true;
61+ } else if (newX >= sceneWidth-1 && newY < m_topBoundaryOffset) { // top right corner
62+ const auto distance = qSqrt(qPow(newX-sceneWidth, 2) + qPow(newY-m_topBoundaryOffset, 2));
63+ Q_EMIT pushedTopRightCorner(qAbs(distance), buttons);
64+ m_pushing = true;
65+ } else if (newX < 0 && newY >= sceneHeight-1) { // bottom left corner
66+ const auto distance = qSqrt(qPow(newX, 2) + qPow(newY-sceneHeight, 2));
67+ Q_EMIT pushedBottomLeftCorner(qAbs(distance), buttons);
68+ m_pushing = true;
69+ } else if (newX >= sceneWidth-1 && newY >= sceneHeight-1) { // bottom right corner
70+ const auto distance = qSqrt(qPow(newX-sceneWidth, 2) + qPow(newY-sceneHeight, 2));
71+ Q_EMIT pushedBottomRightCorner(qAbs(distance), buttons);
72+ m_pushing = true;
73+ } else if (newX < 0) { // left edge
74+ Q_EMIT pushedLeftBoundary(qAbs(newX), buttons);
75+ m_pushing = true;
76+ } else if (newX >= sceneWidth) { // right edge
77+ Q_EMIT pushedRightBoundary(newX - (sceneWidth - 1), buttons);
78+ m_pushing = true;
79+ } else if (newY < m_topBoundaryOffset) { // top edge
80+ Q_EMIT pushedTopBoundary(qAbs(newY - m_topBoundaryOffset), buttons);
81+ m_pushing = true;
82+ } else if (Q_LIKELY(newX > 0 && newX < sceneWidth-1 && newY > 0 && newY < sceneHeight-1)) { // normal pos, not pushing
83+ if (m_pushing) {
84+ Q_EMIT pushStopped();
85+ m_pushing = false;
86+ }
6587 }
6688
6789 applyItemConfinement(newX, newY);
6890
69− setX(newX);
70− setY(newY);
91+ setX(qBound(0.0, newX, sceneWidth - 1));
92+ setY(qBound(0.0, newY, sceneHeight - 1));
7193
7294 QPointF scenePosition = mapToItem(nullptr, QPointF(0, 0));
7395 QWindowSystemInterface::handleMouseEvent(window(), timestamp, scenePosition /*local*/, scenePosition /*global*/,
@@ -110,6 +132,20 @@ void MousePointer::handleWheelEvent(ulong timestamp, QPoint angleDelta, Qt::Keyb
110132 QPoint() /* pixelDelta */, angleDelta, modifiers, Qt::ScrollUpdate);
111133}
112134
135+int MousePointer::topBoundaryOffset() const
136+{
137+ return m_topBoundaryOffset;
138+}
139+
140+void MousePointer::setTopBoundaryOffset(int topBoundaryOffset)
141+{
142+ if (m_topBoundaryOffset == topBoundaryOffset)
143+ return;
144+
145+ m_topBoundaryOffset = topBoundaryOffset;
146+ Q_EMIT topBoundaryOffsetChanged(topBoundaryOffset);
147+}
148+
113149void MousePointer::itemChange(ItemChange change, const ItemChangeData &value)
114150{
115151 if (change == ItemSceneChange) {
plugins/Cursor/MousePointer.h+15−2
@@ -27,9 +27,8 @@
2727
2828class MousePointer : public MirMousePointerInterface {
2929 Q_OBJECT
30−
3130 Q_PROPERTY(QQuickItem* confiningItem READ confiningItem WRITE setConfiningItem NOTIFY confiningItemChanged)
32−
31+ Q_PROPERTY(int topBoundaryOffset READ topBoundaryOffset WRITE setTopBoundaryOffset NOTIFY topBoundaryOffsetChanged)
3332public:
3433 MousePointer(QQuickItem *parent = nullptr);
3534
@@ -44,6 +43,9 @@ public:
4443 QQuickItem* confiningItem() const;
4544 void setConfiningItem(QQuickItem*);
4645
46+ int topBoundaryOffset() const;
47+ void setTopBoundaryOffset(int topBoundaryOffset);
48+
4749public Q_SLOTS:
4850 void handleMouseEvent(ulong timestamp, QPointF movement, Qt::MouseButtons buttons,
4951 Qt::KeyboardModifiers modifiers) override;
@@ -52,9 +54,17 @@ public Q_SLOTS:
5254Q_SIGNALS:
5355 void pushedLeftBoundary(qreal amount, Qt::MouseButtons buttons);
5456 void pushedRightBoundary(qreal amount, Qt::MouseButtons buttons);
57+ void pushedTopBoundary(qreal amount, Qt::MouseButtons buttons);
58+ void pushedTopLeftCorner(qreal amount, Qt::MouseButtons buttons);
59+ void pushedTopRightCorner(qreal amount, Qt::MouseButtons buttons);
60+ void pushedBottomLeftCorner(qreal amount, Qt::MouseButtons buttons);
61+ void pushedBottomRightCorner(qreal amount, Qt::MouseButtons buttons);
62+ void pushStopped();
5563 void mouseMoved();
5664 void confiningItemChanged();
5765
66+ void topBoundaryOffsetChanged(int topBoundaryOffset);
67+
5868protected:
5969 void itemChange(ItemChange change, const ItemChangeData &value) override;
6070
@@ -74,6 +84,9 @@ private:
7484 QPointF m_accumulatedMovement;
7585
7686 QPointer<QQuickItem> m_confiningItem;
87+
88+ int m_topBoundaryOffset{0};
89+ bool m_pushing{false};
7790};
7891
7992#endif // MOUSEPOINTER_H
plugins/Ubuntu/Gestures/TouchGestureArea.h+2−2
@@ -76,7 +76,7 @@ public:
7676 return *this;
7777 }
7878
79− bool operator=(const GestureTouchPoint& rhs) const {
79+ bool operator==(const GestureTouchPoint& rhs) const {
8080 if (&rhs == this) return true;
8181 return m_id == rhs.m_id &&
8282 m_pressed == rhs.m_pressed &&
@@ -84,7 +84,7 @@ public:
8484 m_y == rhs.m_y &&
8585 m_dragging == rhs.m_dragging;
8686 }
87− bool operator!=(const GestureTouchPoint& rhs) const { return !operator=(rhs); }
87+ bool operator!=(const GestureTouchPoint& rhs) const { return !operator==(rhs); }
8888
8989 void setPos(const QPointF &pos);
9090
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/PanelState/PanelState.qml−1
@@ -30,5 +30,4 @@ QtObject {
3030 signal closeClicked()
3131 signal minimizeClicked()
3232 signal restoreClicked()
33− signal focusMaximizedApp()
3433}
qml/Panel/Panel.qml+6−9
@@ -100,15 +100,12 @@ Item {
100100 }
101101 height: indicators.minimizedPanelHeight
102102 hoverEnabled: true
103− onClicked: callHint.visible ? callHint.showLiveCall() : PanelState.focusMaximizedApp()
104− onDoubleClicked: PanelState.restoreClicked()
105−
106− property bool mouseWasPressed: false
107− onPressed: mouseWasPressed = containsPress
108− onMouseYChanged: {
109− if (mouseWasPressed && mouseY > panelHeight) {
110− PanelState.restoreClicked(); // restore the window when "dragging" the panel down
111− mouseWasPressed = false;
103+ onClicked: if (callHint.visible) { callHint.showLiveCall(); }
104+
105+ onPressed: {
106+ if (!callHint.visible) {
107+ // let it fall through to the window decoration of the maximized window behind, if any
108+ mouse.accepted = false;
112109 }
113110 }
114111
qml/Shell.qml+41
@@ -812,6 +812,7 @@ StyledItem {
812812 id: cursor
813813 visible: shell.hasMouse
814814 z: itemGrabber.z + 1
815+ topBoundaryOffset: panel.panelHeight
815816
816817 confiningItem: applicationsDisplayLoader.item ? applicationsDisplayLoader.item.itemConfiningMouseCursor : null
817818
@@ -827,9 +828,16 @@ StyledItem {
827828
828829 height: units.gu(3)
829830
831+ readonly property var previewRectangle: applicationsDisplayLoader.item && applicationsDisplayLoader.item.previewRectangle &&
832+ applicationsDisplayLoader.item.previewRectangle.target &&
833+ applicationsDisplayLoader.item.previewRectangle.target.dragging ?
834+ applicationsDisplayLoader.item.previewRectangle : null
835+
830836 onPushedLeftBoundary: {
831837 if (buttons === Qt.NoButton) {
832838 launcher.pushEdge(amount);
839+ } else if (buttons === Qt.LeftButton && previewRectangle && previewRectangle.target.canBeMaximizedLeftRight) {
840+ previewRectangle.maximizeLeft(amount);
833841 }
834842 }
835843
@@ -837,6 +845,39 @@ StyledItem {
837845 if (buttons === Qt.NoButton && applicationsDisplayLoader.item
838846 && applicationsDisplayLoader.item.pushRightEdge) {
839847 applicationsDisplayLoader.item.pushRightEdge(amount);
848+ } else if (buttons === Qt.LeftButton && previewRectangle && previewRectangle.target.canBeMaximizedLeftRight) {
849+ previewRectangle.maximizeRight(amount);
850+ }
851+ }
852+
853+ onPushedTopBoundary: {
854+ if (buttons === Qt.LeftButton && previewRectangle && previewRectangle.target.canBeMaximized) {
855+ previewRectangle.maximize(amount);
856+ }
857+ }
858+ onPushedTopLeftCorner: {
859+ if (buttons === Qt.LeftButton && previewRectangle && previewRectangle.target.canBeCornerMaximized) {
860+ previewRectangle.maximizeTopLeft(amount);
861+ }
862+ }
863+ onPushedTopRightCorner: {
864+ if (buttons === Qt.LeftButton && previewRectangle && previewRectangle.target.canBeCornerMaximized) {
865+ previewRectangle.maximizeTopRight(amount);
866+ }
867+ }
868+ onPushedBottomLeftCorner: {
869+ if (buttons === Qt.LeftButton && previewRectangle && previewRectangle.target.canBeCornerMaximized) {
870+ previewRectangle.maximizeBottomLeft(amount);
871+ }
872+ }
873+ onPushedBottomRightCorner: {
874+ if (buttons === Qt.LeftButton && previewRectangle && previewRectangle.target.canBeCornerMaximized) {
875+ previewRectangle.maximizeBottomRight(amount);
876+ }
877+ }
878+ onPushStopped: {
879+ if (previewRectangle) {
880+ previewRectangle.stop();
840881 }
841882 }
842883
qml/Stages/DecoratedWindow.qml+18
@@ -52,6 +52,9 @@ FocusScope {
5252 readonly property int heightIncrement: !counterRotate ? applicationWindow.heightIncrement : applicationWindow.widthIncrement
5353
5454 property alias overlayShown: decoration.overlayShown
55+ property alias stageWidth: moveHandler.stageWidth
56+ property alias stageHeight: moveHandler.stageHeight
57+ readonly property alias dragging: moveHandler.dragging
5558
5659 readonly property Item clientAreaItem: applicationWindow
5760
@@ -61,6 +64,7 @@ FocusScope {
6164 signal maximizeVerticallyClicked()
6265 signal minimizeClicked()
6366 signal decorationPressed()
67+ signal decorationReleased()
6468
6569 Rectangle {
6670 id: selectionHighlight
@@ -103,6 +107,20 @@ FocusScope {
103107 onMaximizeVerticallyClicked: { root.decorationPressed(); root.maximizeVerticallyClicked(); }
104108 onMinimizeClicked: root.minimizeClicked();
105109 onPressed: root.decorationPressed();
110+
111+ onPressedChanged: moveHandler.handlePressedChanged(pressed, pressedButtons, mouseX, mouseY)
112+ onPositionChanged: moveHandler.handlePositionChanged(mouse)
113+ onReleased: {
114+ root.decorationReleased();
115+ moveHandler.handleReleased();
116+ }
117+ }
118+
119+ MoveHandler {
120+ id: moveHandler
121+ objectName: "moveHandler"
122+ target: root.parent
123+ buttonsWidth: decoration.buttonsWidth
106124 }
107125
108126 ApplicationWindow {
qml/Stages/DesktopStage.qml+139−69
@@ -45,6 +45,9 @@ AbstractStage {
4545 // Used by TutorialRight
4646 property bool spreadShown: spread.state == "altTab"
4747
48+ // used by the snap windows (edge maximize) feature
49+ readonly property alias previewRectangle: fakeRectangle
50+
4851 mainApp: priv.focusedAppDelegate ? priv.focusedAppDelegate.application : null
4952
5053 // application windows never rotate independently
@@ -73,28 +76,27 @@ AbstractStage {
7376 id: maximizeWindowShortcut
7477 shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Up
7578 onTriggered: priv.focusedAppDelegate.maximize()
76− active: priv.focusedAppDelegate !== null
79+ active: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.canBeMaximized
7780 }
7881
7982 GlobalShortcut {
8083 id: maximizeWindowLeftShortcut
8184 shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Left
8285 onTriggered: priv.focusedAppDelegate.maximizeLeft()
83− active: priv.focusedAppDelegate !== null
86+ active: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.canBeMaximizedLeftRight
8487 }
8588
8689 GlobalShortcut {
8790 id: maximizeWindowRightShortcut
8891 shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Right
8992 onTriggered: priv.focusedAppDelegate.maximizeRight()
90− active: priv.focusedAppDelegate !== null
93+ active: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.canBeMaximizedLeftRight
9194 }
9295
9396 GlobalShortcut {
9497 id: minimizeRestoreShortcut
9598 shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Down
96− onTriggered: priv.focusedAppDelegate.maximized || priv.focusedAppDelegate.maximizedLeft || priv.focusedAppDelegate.maximizedRight ||
97− priv.focusedAppDelegate.maximizedHorizontally || priv.focusedAppDelegate.maximizedVertically
99+ onTriggered: priv.focusedAppDelegate.anyMaximized
98100 ? priv.focusedAppDelegate.restoreFromMaximized() : priv.focusedAppDelegate.minimize()
99101 active: priv.focusedAppDelegate !== null
100102 }
@@ -170,11 +172,6 @@ AbstractStage {
170172 onCloseClicked: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.close(); } }
171173 onMinimizeClicked: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.minimize(); } }
172174 onRestoreClicked: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.restoreFromMaximized(); } }
173− onFocusMaximizedApp: {
174− if (priv.foregroundMaximizedAppDelegate) {
175− priv.foregroundMaximizedAppDelegate.focus = true;
176− }
177− }
178175 }
179176
180177 Binding {
@@ -279,7 +276,7 @@ AbstractStage {
279276 value: appDelegate.requestedY -
280277 Math.min(appDelegate.requestedY - PanelState.panelHeight,
281278 Math.max(0, priv.virtualKeyboardHeight - (appContainer.height - (appDelegate.requestedY + appDelegate.height))))
282− when: root.oskEnabled && appDelegate.focus && appDelegate.state == "normal"
279+ when: root.oskEnabled && appDelegate.focus && (appDelegate.state == "normal" || appDelegate.state == "restored")
283280 && SurfaceManager.inputMethodSurface
284281 && SurfaceManager.inputMethodSurface.state != Mir.HiddenState
285282 && SurfaceManager.inputMethodSurface.state != Mir.MinimizedState
@@ -294,7 +291,7 @@ AbstractStage {
294291 onShellOrientationAngleChanged: {
295292 // at this point decoratedWindow.surfaceOrientationAngle is the old shellOrientationAngle
296293 if (application && application.rotatesWindowContents) {
297− if (state == "normal") {
294+ if (state == "normal" || state == "restored") {
298295 var angleDiff = decoratedWindow.surfaceOrientationAngle - shellOrientationAngle;
299296 angleDiff = (360 + angleDiff) % 360;
300297 if (angleDiff === 90 || angleDiff === 270) {
@@ -320,14 +317,29 @@ AbstractStage {
320317 property int requestedWidth: -1
321318 property int requestedHeight: -1
322319
323− readonly property bool maximized: windowState & WindowStateStorage.WindowStateMaximized
324− readonly property bool maximizedLeft: windowState & WindowStateStorage.WindowStateMaximizedLeft
325− readonly property bool maximizedRight: windowState & WindowStateStorage.WindowStateMaximizedRight
326− readonly property bool maximizedHorizontally: windowState & WindowStateStorage.WindowStateMaximizedHorizontally
327− readonly property bool maximizedVertically: windowState & WindowStateStorage.WindowStateMaximizedVertically
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
325+ readonly property bool maximizedTopLeft: windowState === WindowStateStorage.WindowStateMaximizedTopLeft
326+ readonly property bool maximizedTopRight: windowState === WindowStateStorage.WindowStateMaximizedTopRight
327+ readonly property bool maximizedBottomLeft: windowState === WindowStateStorage.WindowStateMaximizedBottomLeft
328+ readonly property bool maximizedBottomRight: windowState === WindowStateStorage.WindowStateMaximizedBottomRight
329+ readonly property bool anyMaximized: maximized || maximizedLeft || maximizedRight || maximizedHorizontally || maximizedVertically ||
330+ maximizedTopLeft || maximizedTopRight || maximizedBottomLeft || maximizedBottomRight
331+
328332 readonly property bool minimized: windowState & WindowStateStorage.WindowStateMinimized
329333 readonly property alias fullscreen: decoratedWindow.fullscreen
330334
335+ readonly property bool canBeMaximized: canBeMaximizedHorizontally && canBeMaximizedVertically
336+ readonly property bool canBeMaximizedLeftRight: (maximumWidth == 0 || maximumWidth >= appContainer.width/2) &&
337+ (maximumHeight == 0 || maximumHeight >= appContainer.height)
338+ readonly property bool canBeCornerMaximized: (maximumWidth == 0 || maximumWidth >= appContainer.width/2) &&
339+ (maximumHeight == 0 || maximumHeight >= appContainer.height/2)
340+ readonly property bool canBeMaximizedHorizontally: maximumWidth == 0 || maximumWidth >= appContainer.width
341+ readonly property bool canBeMaximizedVertically: maximumHeight == 0 || maximumHeight >= appContainer.height
342+
331343 property int windowState: WindowStateStorage.WindowStateNormal
332344 property bool animationsEnabled: true
333345 property alias title: decoratedWindow.title
@@ -338,6 +350,7 @@ AbstractStage {
338350 readonly property var surface: model.surface
339351 readonly property alias resizeArea: resizeArea
340352 readonly property alias focusedSurface: decoratedWindow.focusedSurface
353+ readonly property bool dragging: touchControls.overlayShown ? touchControls.dragging : decoratedWindow.dragging
341354
342355 readonly property bool isDash: model.application.appId == "unity8-dash"
343356 readonly property alias clientAreaItem: decoratedWindow.clientAreaItem
@@ -346,7 +359,7 @@ AbstractStage {
346359 if (spread.state == "altTab") {
347360 spread.cancel();
348361 }
349− appDelegate.restore();
362+ appDelegate.restore(true /* animated */, appDelegate.windowState);
350363 }
351364 Connections {
352365 target: model.surface
@@ -454,28 +467,34 @@ AbstractStage {
454467 animationsEnabled = (animated === undefined) || animated;
455468 windowState = WindowStateStorage.WindowStateMaximizedVertically;
456469 }
470+ function maximizeTopLeft(animated) {
471+ animationsEnabled = (animated === undefined) || animated;
472+ windowState = WindowStateStorage.WindowStateMaximizedTopLeft;
473+ }
474+ function maximizeTopRight(animated) {
475+ animationsEnabled = (animated === undefined) || animated;
476+ windowState = WindowStateStorage.WindowStateMaximizedTopRight;
477+ }
478+ function maximizeBottomLeft(animated) {
479+ animationsEnabled = (animated === undefined) || animated;
480+ windowState = WindowStateStorage.WindowStateMaximizedBottomLeft;
481+ }
482+ function maximizeBottomRight(animated) {
483+ animationsEnabled = (animated === undefined) || animated;
484+ windowState = WindowStateStorage.WindowStateMaximizedBottomRight;
485+ }
457486 function minimize(animated) {
458487 animationsEnabled = (animated === undefined) || animated;
459488 windowState |= WindowStateStorage.WindowStateMinimized; // add the minimized bit
460489 }
461490 function restoreFromMaximized(animated) {
462491 animationsEnabled = (animated === undefined) || animated;
463− windowState = WindowStateStorage.WindowStateNormal;
492+ windowState = WindowStateStorage.WindowStateRestored;
464493 }
465− function restore(animated) {
494+ function restore(animated,state) {
466495 animationsEnabled = (animated === undefined) || animated;
496+ windowState = state || WindowStateStorage.WindowStateRestored;
467497 windowState &= ~WindowStateStorage.WindowStateMinimized; // clear the minimized bit
468− if (maximized)
469− maximize();
470− else if (maximizedLeft)
471− maximizeLeft();
472− else if (maximizedRight)
473− maximizeRight();
474− else if (maximizedHorizontally)
475− maximizeHorizontally();
476− else if (maximizedVertically)
477− maximizeVertically();
478−
479498 focus = true;
480499 }
481500
@@ -492,9 +511,14 @@ AbstractStage {
492511 duration: UbuntuAnimation.SnapDuration
493512 }
494513
514+ // unlike requestedX/Y, this is the last known grab position before being pushed against edges/corners
515+ // when restoring, the window should return to these, not to the place where it was dropped near the edge
516+ property real restoredX
517+ property real restoredY
518+
495519 states: [
496520 State {
497− name: "fullscreen"; when: decoratedWindow.fullscreen && !appDelegate.minimized
521+ name: "fullscreen"; when: appDelegate.fullscreen && !appDelegate.minimized
498522 PropertyChanges {
499523 target: appDelegate;
500524 x: rotation == 0 ? 0 : (parent.width - width) / 2 + (shellOrientationAngle == 90 ? -PanelState.panelHeight : PanelState.panelHeight)
@@ -513,61 +537,94 @@ AbstractStage {
513537 }
514538 },
515539 State {
516− name: "maximized"; when: appDelegate.maximized && !appDelegate.minimized
540+ name: "restored";
541+ when: appDelegate.windowState == WindowStateStorage.WindowStateRestored
542+ extend: "normal"
517543 PropertyChanges {
518544 target: appDelegate;
519− x: root.leftMargin;
520− y: 0;
521− visuallyMinimized: false;
522− visuallyMaximized: true
545+ requestedX: restoredX;
546+ requestedY: restoredY;
523547 }
548+ },
549+ State {
550+ name: "maximized"; when: appDelegate.maximized && !appDelegate.minimized
524551 PropertyChanges {
525− target: decoratedWindow
552+ target: appDelegate;
553+ requestedX: root.leftMargin;
554+ requestedY: 0;
526555 requestedWidth: appContainer.width - root.leftMargin;
527556 requestedHeight: appContainer.height;
557+ visuallyMinimized: false;
558+ visuallyMaximized: true
528559 }
529560 },
530561 State {
531562 name: "maximizedLeft"; when: appDelegate.maximizedLeft && !appDelegate.minimized
532563 PropertyChanges {
533564 target: appDelegate
534− x: root.leftMargin
535− y: PanelState.panelHeight
536− }
537− PropertyChanges {
538− target: decoratedWindow
565+ requestedX: root.leftMargin
566+ requestedY: PanelState.panelHeight
539567 requestedWidth: (appContainer.width - root.leftMargin)/2
540568 requestedHeight: appContainer.height - PanelState.panelHeight
541569 }
542570 },
543571 State {
544572 name: "maximizedRight"; when: appDelegate.maximizedRight && !appDelegate.minimized
573+ extend: "maximizedLeft"
545574 PropertyChanges {
546575 target: appDelegate;
547− x: (appContainer.width + root.leftMargin)/2
548− y: PanelState.panelHeight
576+ requestedX: (appContainer.width + root.leftMargin)/2
549577 }
578+ },
579+ State {
580+ name: "maximizedTopLeft"; when: appDelegate.maximizedTopLeft && !appDelegate.minimized
550581 PropertyChanges {
551− target: decoratedWindow
582+ target: appDelegate
583+ requestedX: root.leftMargin
584+ requestedY: PanelState.panelHeight
552585 requestedWidth: (appContainer.width - root.leftMargin)/2
553− requestedHeight: appContainer.height - PanelState.panelHeight
586+ requestedHeight: (appContainer.height - PanelState.panelHeight)/2
587+ }
588+ },
589+ State {
590+ name: "maximizedTopRight"; when: appDelegate.maximizedTopRight && !appDelegate.minimized
591+ extend: "maximizedTopLeft"
592+ PropertyChanges {
593+ target: appDelegate
594+ requestedX: (appContainer.width + root.leftMargin)/2
595+ }
596+ },
597+ State {
598+ name: "maximizedBottomLeft"; when: appDelegate.maximizedBottomLeft && !appDelegate.minimized
599+ PropertyChanges {
600+ target: appDelegate
601+ requestedX: root.leftMargin
602+ requestedY: (appContainer.height + PanelState.panelHeight)/2
603+ requestedWidth: (appContainer.width - root.leftMargin)/2
604+ requestedHeight: appContainer.height/2
605+ }
606+ },
607+ State {
608+ name: "maximizedBottomRight"; when: appDelegate.maximizedBottomRight && !appDelegate.minimized
609+ extend: "maximizedBottomLeft"
610+ PropertyChanges {
611+ target: appDelegate
612+ requestedX: (appContainer.width + root.leftMargin)/2
554613 }
555614 },
556615 State {
557616 name: "maximizedHorizontally"; when: appDelegate.maximizedHorizontally && !appDelegate.minimized
558− PropertyChanges { target: appDelegate; x: root.leftMargin }
559− PropertyChanges { target: decoratedWindow; requestedWidth: appContainer.width - root.leftMargin }
617+ PropertyChanges { target: appDelegate; requestedX: root.leftMargin; requestedY: requestedY; requestedWidth: appContainer.width - root.leftMargin }
560618 },
561619 State {
562620 name: "maximizedVertically"; when: appDelegate.maximizedVertically && !appDelegate.minimized
563− PropertyChanges { target: appDelegate; y: PanelState.panelHeight }
564− PropertyChanges { target: decoratedWindow; requestedHeight: appContainer.height - PanelState.panelHeight }
621+ PropertyChanges { target: appDelegate; requestedX: requestedX; requestedY: PanelState.panelHeight; requestedHeight: appContainer.height - PanelState.panelHeight }
565622 },
566623 State {
567624 name: "minimized"; when: appDelegate.minimized
568625 PropertyChanges {
569626 target: appDelegate;
570− x: -appDelegate.width / 2;
627+ requestedX: -appDelegate.width / 2;
571628 scale: units.gu(5) / appDelegate.width;
572629 opacity: 0;
573630 visuallyMinimized: true;
@@ -577,21 +634,17 @@ AbstractStage {
577634 ]
578635 transitions: [
579636 Transition {
580− to: "normal"
637+ to: "normal,restored"
581638 enabled: appDelegate.animationsEnabled
582639 PropertyAction { target: appDelegate; properties: "visuallyMinimized,visuallyMaximized" }
583− UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,requestedWidth,requestedHeight,scale"; duration: UbuntuAnimation.FastDuration }
584− UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
640+ UbuntuNumberAnimation { target: appDelegate; properties: "requestedX,requestedY,opacity,scale,requestedWidth,requestedHeight" }
585641 },
586642 Transition {
587643 to: "minimized"
588644 enabled: appDelegate.animationsEnabled
589645 PropertyAction { target: appDelegate; property: "visuallyMaximized" }
590646 SequentialAnimation {
591− ParallelAnimation {
592− UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,scale"; duration: UbuntuAnimation.FastDuration }
593− UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
594− }
647+ UbuntuNumberAnimation { target: appDelegate; properties: "requestedX,requestedY,opacity,scale,requestedWidth,requestedHeight" }
595648 PropertyAction { target: appDelegate; property: "visuallyMinimized" }
596649 ScriptAction {
597650 script: {
@@ -608,11 +661,9 @@ AbstractStage {
608661 enabled: appDelegate.animationsEnabled
609662 PropertyAction { target: appDelegate; property: "visuallyMinimized" }
610663 SequentialAnimation {
611− ParallelAnimation {
612− UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,scale"; duration: UbuntuAnimation.FastDuration }
613− UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
614− }
664+ UbuntuNumberAnimation { target: appDelegate; properties: "requestedX,requestedY,opacity,scale,requestedWidth,requestedHeight" }
615665 PropertyAction { target: appDelegate; property: "visuallyMaximized" }
666+ ScriptAction { script: { fakeRectangle.stop(); } }
616667 }
617668 }
618669 ]
@@ -678,27 +729,38 @@ AbstractStage {
678729 surface: model.surface
679730 active: appDelegate.focus
680731 focus: true
681− maximizeButtonShown: (maximumWidth == 0 || maximumWidth >= appContainer.width) &&
682− (maximumHeight == 0 || maximumHeight >= appContainer.height)
732+ maximizeButtonShown: appDelegate.canBeMaximized
683733 overlayShown: touchControls.overlayShown
734+ stageWidth: appContainer.width
735+ stageHeight: appContainer.height
684736
685737 requestedWidth: appDelegate.requestedWidth
686738 requestedHeight: appDelegate.requestedHeight
687739
688740 onCloseClicked: { appDelegate.close(); }
689− onMaximizeClicked: appDelegate.maximized || appDelegate.maximizedLeft || appDelegate.maximizedRight
690− || appDelegate.maximizedHorizontally || appDelegate.maximizedVertically
691− ? appDelegate.restoreFromMaximized() : appDelegate.maximize()
741+ onMaximizeClicked: appDelegate.anyMaximized ? appDelegate.restoreFromMaximized() : appDelegate.maximize();
692742 onMaximizeHorizontallyClicked: appDelegate.maximizedHorizontally ? appDelegate.restoreFromMaximized() : appDelegate.maximizeHorizontally()
693743 onMaximizeVerticallyClicked: appDelegate.maximizedVertically ? appDelegate.restoreFromMaximized() : appDelegate.maximizeVertically()
694744 onMinimizeClicked: appDelegate.minimize()
695745 onDecorationPressed: { appDelegate.focus = true; }
746+ onDecorationReleased: fakeRectangle.commit();
696747 }
697748
698749 WindowControlsOverlay {
699750 id: touchControls
700− anchors.fill: appDelegate
701751 target: appDelegate
752+ stageWidth: appContainer.width
753+ stageHeight: appContainer.height
754+
755+ onFakeMaximizeAnimationRequested: if (!appDelegate.maximized) fakeRectangle.maximize(amount, true)
756+ onFakeMaximizeLeftAnimationRequested: if (!appDelegate.maximizedLeft) fakeRectangle.maximizeLeft(amount, true)
757+ onFakeMaximizeRightAnimationRequested: if (!appDelegate.maximizedRight) fakeRectangle.maximizeRight(amount, true)
758+ onFakeMaximizeTopLeftAnimationRequested: if (!appDelegate.maximizedTopLeft) fakeRectangle.maximizeTopLeft(amount, true);
759+ onFakeMaximizeTopRightAnimationRequested: if (!appDelegate.maximizedTopRight) fakeRectangle.maximizeTopRight(amount, true);
760+ onFakeMaximizeBottomLeftAnimationRequested: if (!appDelegate.maximizedBottomLeft) fakeRectangle.maximizeBottomLeft(amount, true);
761+ onFakeMaximizeBottomRightAnimationRequested: if (!appDelegate.maximizedBottomRight) fakeRectangle.maximizeBottomRight(amount, true);
762+ onStopFakeAnimation: fakeRectangle.stop();
763+ onDragReleased: fakeRectangle.commit();
702764 }
703765
704766 WindowedFullscreenPolicy {
@@ -710,6 +772,14 @@ AbstractStage {
710772 }
711773 }
712774
775+ FakeMaximizeDelegate {
776+ id: fakeRectangle
777+ target: priv.focusedAppDelegate
778+ leftMargin: root.leftMargin
779+ appContainerWidth: appContainer.width
780+ appContainerHeight: appContainer.height
781+ }
782+
713783 EdgeBarrier {
714784 id: edgeBarrier
715785
qml/Stages/FakeMaximizeDelegate.qmlnew file+233
@@ -0,0 +1,233 @@
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 Utils 0.1 // For EdgeBarrierSettings
20+import "../Components/PanelState"
21+
22+Rectangle {
23+ id: fakeRectangle
24+ visible: opacity > 0 && target && !target.anyMaximized // we go from 0.2 to 0.5
25+ enabled: visible
26+ color: "#ffffff"
27+ border.width: units.dp(2)
28+ border.color: "#99ffffff"
29+
30+ scale: progress > 0 && progress <= hintThreshold ? MathUtils.projectValue(progress, 0.0, 1.0, 1, 2) : 1
31+ opacity: progress > 0 ? MathUtils.projectValue(progress, 0.0, 1.0, 0.2, 0.5) : 0
32+
33+ property int edge: -1 // Item.TransformOrigin
34+ property var target // appDelegate
35+ property int leftMargin
36+ property real appContainerWidth
37+ property real appContainerHeight
38+
39+ readonly property real hintThreshold: 0.1
40+
41+ // Edge push progress
42+ // Value range is [0.0, 1.0]
43+ readonly property real progress: priv.directProgress != -1 ? priv.directProgress : priv.accumulatedProgress
44+
45+ signal passed(int origin)
46+
47+ QtObject {
48+ id: priv
49+
50+ readonly property real accumulatedProgress: MathUtils.clamp(accumulatedPush / EdgeBarrierSettings.pushThreshold, 0.0, 1.0)
51+ property real directProgress: -1
52+ property real accumulatedPush: 0
53+
54+ function push(amount) {
55+ if (accumulatedPush === EdgeBarrierSettings.pushThreshold) {
56+ // NO-OP
57+ return;
58+ }
59+
60+ if (accumulatedPush + amount > EdgeBarrierSettings.pushThreshold) {
61+ accumulatedPush = EdgeBarrierSettings.pushThreshold;
62+ } else {
63+ accumulatedPush += amount;
64+ }
65+
66+ if (accumulatedPush === EdgeBarrierSettings.pushThreshold) {
67+ passed(edge);
68+ // commit(); // NB: uncomment to have automatic maximization on 100% progress
69+ }
70+ }
71+
72+ function setup(edge) {
73+ if (edge !== fakeRectangle.edge) {
74+ stop(); // a different edge, start anew
75+ }
76+ fakeRectangle.x = target.x;
77+ fakeRectangle.y = target.y;
78+ fakeRectangle.width = target.width;
79+ fakeRectangle.height = target.height;
80+ fakeRectangle.edge = edge;
81+ fakeRectangle.transformOrigin = edge;
82+ }
83+
84+ function processAnimation(amount, animation, isProgress) {
85+ if (isProgress) {
86+ priv.directProgress = amount;
87+ } else {
88+ priv.directProgress = -1;
89+ priv.push(amount);
90+ }
91+
92+ if (progress > hintThreshold) { // above 10% we start the full preview animation
93+ animation.start();
94+ }
95+ }
96+ }
97+
98+ function commit() {
99+ if (progress > hintThreshold && edge != -1) {
100+ if (edge == Item.Top) {
101+ target.maximize();
102+ } else if (edge == Item.Left) {
103+ target.maximizeLeft();
104+ } else if (edge == Item.Right) {
105+ target.maximizeRight();
106+ } else if (edge == Item.TopLeft) {
107+ target.maximizeTopLeft();
108+ } else if (edge == Item.TopRight) {
109+ target.maximizeTopRight();
110+ } else if (edge == Item.BottomLeft) {
111+ target.maximizeBottomLeft();
112+ } else if (edge == Item.BottomRight) {
113+ target.maximizeBottomRight();
114+ }
115+ } else {
116+ stop();
117+ }
118+ }
119+
120+ function stop() {
121+ priv.accumulatedPush = 0;
122+ priv.directProgress = -1;
123+ edge = -1;
124+ }
125+
126+ function maximize(amount, isProgress) {
127+ if (fakeRectangle.edge != Item.Top) {
128+ priv.setup(Item.Top);
129+ }
130+ priv.processAnimation(amount, fakeMaximizeAnimation, isProgress);
131+ }
132+
133+ function maximizeLeft(amount, isProgress) {
134+ if (fakeRectangle.edge != Item.Left) {
135+ priv.setup(Item.Left);
136+ }
137+ priv.processAnimation(amount, fakeMaximizeLeftAnimation, isProgress);
138+ }
139+
140+ function maximizeRight(amount, isProgress) {
141+ if (fakeRectangle.edge != Item.Right) {
142+ priv.setup(Item.Right);
143+ }
144+ priv.processAnimation(amount, fakeMaximizeRightAnimation, isProgress);
145+ }
146+
147+ function maximizeTopLeft(amount, isProgress) {
148+ if (fakeRectangle.edge != Item.TopLeft) {
149+ priv.setup(Item.TopLeft);
150+ }
151+ priv.processAnimation(amount, fakeMaximizeTopLeftAnimation, isProgress);
152+ }
153+
154+ function maximizeTopRight(amount, isProgress) {
155+ if (fakeRectangle.edge != Item.TopRight) {
156+ priv.setup(Item.TopRight);
157+ }
158+ priv.processAnimation(amount, fakeMaximizeTopRightAnimation, isProgress);
159+ }
160+
161+ function maximizeBottomLeft(amount, isProgress) {
162+ if (fakeRectangle.edge != Item.BottomLeft) {
163+ priv.setup(Item.BottomLeft);
164+ }
165+ priv.processAnimation(amount, fakeMaximizeBottomLeftAnimation, isProgress);
166+ }
167+
168+ function maximizeBottomRight(amount, isProgress) {
169+ if (fakeRectangle.edge != Item.BottomRight) {
170+ priv.setup(Item.BottomRight);
171+ }
172+ priv.processAnimation(amount, fakeMaximizeBottomRightAnimation, isProgress);
173+ }
174+
175+ Behavior on opacity { UbuntuNumberAnimation { duration: UbuntuAnimation.BriskDuration } }
176+ Behavior on scale { UbuntuNumberAnimation { duration: UbuntuAnimation.BriskDuration } }
177+
178+ ParallelAnimation {
179+ id: fakeMaximizeAnimation
180+ UbuntuNumberAnimation { target: fakeRectangle; properties: "x"; duration: UbuntuAnimation.BriskDuration; to: leftMargin }
181+ UbuntuNumberAnimation { target: fakeRectangle; properties: "y"; duration: UbuntuAnimation.BriskDuration; to: PanelState.panelHeight }
182+ UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: appContainerWidth - leftMargin }
183+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: appContainerHeight }
184+ }
185+
186+ ParallelAnimation {
187+ id: fakeMaximizeLeftAnimation
188+ UbuntuNumberAnimation { target: fakeRectangle; properties: "x"; duration: UbuntuAnimation.BriskDuration; to: leftMargin }
189+ UbuntuNumberAnimation { target: fakeRectangle; properties: "y"; duration: UbuntuAnimation.BriskDuration; to: PanelState.panelHeight }
190+ UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth - leftMargin)/2 }
191+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: appContainerHeight - PanelState.panelHeight }
192+ }
193+
194+ ParallelAnimation {
195+ id: fakeMaximizeRightAnimation
196+ UbuntuNumberAnimation { target: fakeRectangle; properties: "x"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth + leftMargin)/2 }
197+ UbuntuNumberAnimation { target: fakeRectangle; properties: "y"; duration: UbuntuAnimation.BriskDuration; to: PanelState.panelHeight }
198+ UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth - leftMargin)/2 }
199+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: appContainerHeight - PanelState.panelHeight }
200+ }
201+
202+ ParallelAnimation {
203+ id: fakeMaximizeTopLeftAnimation
204+ UbuntuNumberAnimation { target: fakeRectangle; properties: "x"; duration: UbuntuAnimation.BriskDuration; to: leftMargin }
205+ UbuntuNumberAnimation { target: fakeRectangle; properties: "y"; duration: UbuntuAnimation.BriskDuration; to: PanelState.panelHeight }
206+ UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth - leftMargin)/2 }
207+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: (appContainerHeight - PanelState.panelHeight)/2 }
208+ }
209+
210+ ParallelAnimation {
211+ id: fakeMaximizeTopRightAnimation
212+ UbuntuNumberAnimation { target: fakeRectangle; properties: "x"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth + leftMargin)/2 }
213+ UbuntuNumberAnimation { target: fakeRectangle; properties: "y"; duration: UbuntuAnimation.BriskDuration; to: PanelState.panelHeight }
214+ UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth - leftMargin)/2 }
215+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: (appContainerHeight - PanelState.panelHeight)/2 }
216+ }
217+
218+ ParallelAnimation {
219+ id: fakeMaximizeBottomLeftAnimation
220+ UbuntuNumberAnimation { target: fakeRectangle; properties: "x"; duration: UbuntuAnimation.BriskDuration; to: leftMargin }
221+ UbuntuNumberAnimation { target: fakeRectangle; properties: "y"; duration: UbuntuAnimation.BriskDuration; to: (appContainerHeight + PanelState.panelHeight)/2 }
222+ UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth - leftMargin)/2 }
223+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: appContainerHeight/2 }
224+ }
225+
226+ ParallelAnimation {
227+ id: fakeMaximizeBottomRightAnimation
228+ UbuntuNumberAnimation { target: fakeRectangle; properties: "x"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth + leftMargin)/2 }
229+ UbuntuNumberAnimation { target: fakeRectangle; properties: "y"; duration: UbuntuAnimation.BriskDuration; to: (appContainerHeight + PanelState.panelHeight)/2 }
230+ UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth - leftMargin)/2 }
231+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: appContainerHeight/2 }
232+ }
233+}
qml/Stages/MoveHandler.qmlnew file+179
@@ -0,0 +1,179 @@
1+/*
2+ * Copyright (C) 2014-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 Unity.Application 0.1 // For Mir singleton
19+import Ubuntu.Components 1.3
20+import Utils 0.1
21+import "../Components"
22+import "../Components/PanelState"
23+
24+QtObject {
25+ id: root
26+
27+ property Item target // appDelegate
28+ property int stageWidth
29+ property int stageHeight
30+ property real buttonsWidth: 0
31+
32+ readonly property bool dragging: priv.dragging
33+
34+ signal fakeMaximizeAnimationRequested(real amount)
35+ signal fakeMaximizeLeftAnimationRequested(real amount)
36+ signal fakeMaximizeRightAnimationRequested(real amount)
37+ signal fakeMaximizeTopLeftAnimationRequested(real amount)
38+ signal fakeMaximizeTopRightAnimationRequested(real amount)
39+ signal fakeMaximizeBottomLeftAnimationRequested(real amount)
40+ signal fakeMaximizeBottomRightAnimationRequested(real amount)
41+ signal stopFakeAnimation()
42+
43+ property QtObject priv: QtObject {
44+ property real distanceX
45+ property real distanceY
46+ property bool dragging
47+
48+ readonly property int triggerArea: units.gu(8)
49+ property bool nearLeftEdge: target.maximizedLeft
50+ property bool nearTopEdge: target.maximized
51+ property bool nearRightEdge: target.maximizedRight
52+ property bool nearTopLeftCorner: target.maximizedTopLeft
53+ property bool nearTopRightCorner: target.maximizedTopRight
54+ property bool nearBottomLeftCorner: target.maximizedBottomLeft
55+ property bool nearBottomRightCorner: target.maximizedBottomRight
56+
57+ function resetEdges() {
58+ nearLeftEdge = false;
59+ nearRightEdge = false;
60+ nearTopEdge = false;
61+ nearTopLeftCorner = false;
62+ nearTopRightCorner = false;
63+ nearBottomLeftCorner = false;
64+ nearBottomRightCorner = false;
65+ }
66+
67+ // return the progress of mouse pointer movement from 0 to 1 within a corner square of the screen
68+ // 0 -> before the mouse enters the square
69+ // 1 -> mouse is in the outer corner
70+ // a is the corner, b is the mouse pos
71+ function progressInCorner(ax, ay, bx, by) {
72+ // distance of two points, a and b, in pixels
73+ var distance = Math.sqrt(Math.pow(bx-ax, 2) + Math.pow(by-ay, 2));
74+ // length of the triggerArea square diagonal
75+ var diagLength = Math.sqrt(2 * priv.triggerArea * priv.triggerArea);
76+ var ratio = 1 - (distance / diagLength);
77+ return bx > 0 && bx <= stageWidth && by > 0 && by <= stageHeight ? ratio : 1; // everything "outside" of our square from the center is 1
78+ }
79+ property real progress: 0
80+ }
81+
82+ function handlePressedChanged(pressed, pressedButtons, mouseX, mouseY) {
83+ if (pressed && pressedButtons === Qt.LeftButton) {
84+ var pos = mapToItem(target, mouseX, mouseY);
85+ if (target.anyMaximized) {
86+ // keep distanceX relative to the normal window width minus the window control buttons (+spacing)
87+ // so that dragging it back doesn't make the window jump around to weird positions, away from the mouse pointer
88+ priv.distanceX = MathUtils.clampAndProject(pos.x, 0, target.width, buttonsWidth, target.resizeArea.normalWidth);
89+ priv.distanceY = MathUtils.clampAndProject(pos.y, 0, target.height, 0, target.resizeArea.normalHeight);
90+ } else {
91+ priv.distanceX = pos.x;
92+ priv.distanceY = pos.y;
93+ }
94+
95+ priv.dragging = true;
96+ } else {
97+ priv.dragging = false;
98+ Mir.cursorName = "";
99+ }
100+ }
101+
102+ function handlePositionChanged(mouse, sensingPoints) {
103+ if (priv.dragging) {
104+ Mir.cursorName = "grabbing";
105+
106+ if (target.anyMaximized) { // restore from maximized when dragging away from edges/corners
107+ priv.progress = 0;
108+ target.restore(false, WindowStateStorage.WindowStateNormal);
109+ }
110+
111+ var pos = mapToItem(target.parent, mouse.x, mouse.y);
112+ // Use integer coordinate values to ensure that target is left in a pixel-aligned
113+ // position. Mouse movement could have subpixel precision, yielding a fractional
114+ // mouse position.
115+ target.requestedX = Math.round(pos.x - priv.distanceX);
116+ target.requestedY = Math.round(Math.max(pos.y - priv.distanceY, PanelState.panelHeight));
117+
118+ if (sensingPoints) { // edge/corner detection when dragging via the touch overlay
119+ if (sensingPoints.topLeft.x < priv.triggerArea && sensingPoints.topLeft.y < PanelState.panelHeight + priv.triggerArea
120+ && target.canBeCornerMaximized) { // top left
121+ priv.progress = priv.progressInCorner(0, PanelState.panelHeight, sensingPoints.topLeft.x, sensingPoints.topLeft.y);
122+ priv.resetEdges();
123+ priv.nearTopLeftCorner = true;
124+ root.fakeMaximizeTopLeftAnimationRequested(priv.progress);
125+ } else if (sensingPoints.topRight.x > stageWidth - priv.triggerArea && sensingPoints.topRight.y < PanelState.panelHeight + priv.triggerArea
126+ && target.canBeCornerMaximized) { // top right
127+ priv.progress = priv.progressInCorner(stageWidth, PanelState.panelHeight, sensingPoints.topRight.x, sensingPoints.topRight.y);
128+ priv.resetEdges();
129+ priv.nearTopRightCorner = true;
130+ root.fakeMaximizeTopRightAnimationRequested(priv.progress);
131+ } else if (sensingPoints.bottomLeft.x < priv.triggerArea && sensingPoints.bottomLeft.y > stageHeight - priv.triggerArea
132+ && target.canBeCornerMaximized) { // bottom left
133+ priv.progress = priv.progressInCorner(0, stageHeight, sensingPoints.bottomLeft.x, sensingPoints.bottomLeft.y);
134+ priv.resetEdges();
135+ priv.nearBottomLeftCorner = true;
136+ root.fakeMaximizeBottomLeftAnimationRequested(priv.progress);
137+ } else if (sensingPoints.bottomRight.x > stageWidth - priv.triggerArea && sensingPoints.bottomRight.y > stageHeight - priv.triggerArea
138+ && target.canBeCornerMaximized) { // bottom right
139+ priv.progress = priv.progressInCorner(stageWidth, stageHeight, sensingPoints.bottomRight.x, sensingPoints.bottomRight.y);
140+ priv.resetEdges();
141+ priv.nearBottomRightCorner = true;
142+ root.fakeMaximizeBottomRightAnimationRequested(priv.progress);
143+ } else if (sensingPoints.left.x < priv.triggerArea && target.canBeMaximizedLeftRight) { // left
144+ priv.progress = MathUtils.clampAndProject(sensingPoints.left.x, priv.triggerArea, 0, 0, 1);
145+ priv.resetEdges();
146+ priv.nearLeftEdge = true;
147+ root.fakeMaximizeLeftAnimationRequested(priv.progress);
148+ } else if (sensingPoints.right.x > stageWidth - priv.triggerArea && target.canBeMaximizedLeftRight) { // right
149+ priv.progress = MathUtils.clampAndProject(sensingPoints.right.x, stageWidth - priv.triggerArea, stageWidth, 0, 1);
150+ priv.resetEdges();
151+ priv.nearRightEdge = true;
152+ root.fakeMaximizeRightAnimationRequested(priv.progress);
153+ } else if (sensingPoints.top.y < PanelState.panelHeight + priv.triggerArea && target.canBeMaximized) { // top
154+ priv.progress = MathUtils.clampAndProject(sensingPoints.top.y, PanelState.panelHeight + priv.triggerArea, 0, 0, 1);
155+ priv.resetEdges();
156+ priv.nearTopEdge = true;
157+ root.fakeMaximizeAnimationRequested(priv.progress);
158+ } else if (priv.nearLeftEdge || priv.nearRightEdge || priv.nearTopEdge || priv.nearTopLeftCorner || priv.nearTopRightCorner ||
159+ priv.nearBottomLeftCorner || priv.nearBottomRightCorner) {
160+ priv.progress = 0;
161+ priv.resetEdges();
162+ root.stopFakeAnimation();
163+ }
164+ }
165+ }
166+ }
167+
168+ function handleReleased(touchMode) {
169+ if (touchMode) {
170+ priv.progress = 0;
171+ priv.resetEdges();
172+ }
173+ if ((target.state == "normal" || target.state == "restored") && priv.progress == 0) {
174+ // save the x/y to restore to
175+ target.restoredX = target.x;
176+ target.restoredY = target.y;
177+ }
178+ }
179+}
qml/Stages/TopLevelSurfaceRepeater.qml+7−1
@@ -27,7 +27,13 @@ Repeater {
2727 if (!startingUp) {
2828 // the top-most surface must be the focused one.
2929 var topmostDelegate = itemAt(0);
30− topmostDelegate.focus = true;
30+ if (topmostDelegate.focus) {
31+ // Delegate got focused while we were still starting up. Not good.
32+ // Force signal handler to run again
33+ topmostDelegate.onFocusChanged(true);
34+ } else {
35+ topmostDelegate.focus = true;
36+ }
3137 }
3238 }
3339
qml/Stages/WindowControlsOverlay.qml+76−38
@@ -23,12 +23,26 @@ 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 alias stageWidth: moveHandler.stageWidth
31+ property alias stageHeight: moveHandler.stageHeight
2932
3033 // to be read from outside
3134 readonly property alias overlayShown: overlay.visible
35+ readonly property alias dragging: priv.dragging
36+
37+ signal fakeMaximizeAnimationRequested(real amount)
38+ signal fakeMaximizeLeftAnimationRequested(real amount)
39+ signal fakeMaximizeRightAnimationRequested(real amount)
40+ signal fakeMaximizeTopLeftAnimationRequested(real amount)
41+ signal fakeMaximizeTopRightAnimationRequested(real amount)
42+ signal fakeMaximizeBottomLeftAnimationRequested(real amount)
43+ signal fakeMaximizeBottomRightAnimationRequested(real amount)
44+ signal stopFakeAnimation()
45+ signal dragReleased()
3246
3347 TouchGestureArea {
3448 id: gestureArea
@@ -51,16 +65,18 @@ Item {
5165 readonly property bool recognizedDrag: recognizedPress && dragging
5266 onRecognizedDragChanged: {
5367 if (recognizedDrag) {
54− priv.handlePressedChanged(true, tp.x, tp.y);
55− } else if (!moveHandler.containsPress) { // prevent interfering with the central piece drag/move
56− priv.dragging = false;
68+ moveHandler.handlePressedChanged(true, Qt.LeftButton, tp.x, tp.y);
69+ } else if (!mouseArea.containsPress) { // prevent interfering with the central piece drag/move
70+ moveHandler.handlePressedChanged(false, Qt.LeftButton);
71+ root.dragReleased();
72+ moveHandler.handleReleased(true);
5773 }
5874 }
5975
6076 readonly property point tp: recognizedPress ? Qt.point(touchPoints[0].x, touchPoints[0].y) : Qt.point(-1, -1)
6177 onUpdated: {
6278 if (recognizedDrag) {
63− priv.handlePositionChanged(tp.x, tp.y);
79+ moveHandler.handlePositionChanged(tp, priv.getSensingPoints());
6480 }
6581 }
6682 }
@@ -69,33 +85,37 @@ Item {
6985 Timer {
7086 id: overlayTimer
7187 interval: 2000
72− repeat: priv.dragging || (priv.resizeArea && priv.resizeArea.dragging)
88+ repeat: priv.dragging
7389 }
7490
7591 QtObject {
7692 id: priv
77− property real distanceX
78− property real distanceY
79− property bool dragging
80−
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+ readonly property bool dragging: moveHandler.dragging || (resizeArea && resizeArea.dragging)
8296
83− function handlePressedChanged(pressed, mouseX, mouseY) {
84− if (pressed) {
85− var pos = mapToItem(root.target, mouseX, mouseY);
86− priv.distanceX = pos.x;
87− priv.distanceY = pos.y;
88− priv.dragging = true;
89− } else {
90− priv.dragging = false;
97+ function getSensingPoints() {
98+ var xPoints = [];
99+ var yPoints = [];
100+ for (var i = 0; i < gestureArea.touchPoints.length; i++) {
101+ var pt = gestureArea.touchPoints[i];
102+ xPoints.push(pt.x);
103+ yPoints.push(pt.y);
91104 }
92− }
93105
94− 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));
106+ var leftmost = Math.min.apply(Math, xPoints);
107+ var rightmost = Math.max.apply(Math, xPoints);
108+ var topmost = Math.min.apply(Math, yPoints);
109+ var bottommost = Math.max.apply(Math, yPoints);
110+
111+ return {
112+ left: mapToItem(target.parent, leftmost, (topmost+bottommost)/2),
113+ top: mapToItem(target.parent, (leftmost+rightmost)/2, topmost),
114+ right: mapToItem(target.parent, rightmost, (topmost+bottommost)/2),
115+ topLeft: mapToItem(target.parent, leftmost, topmost),
116+ topRight: mapToItem(target.parent, rightmost, topmost),
117+ bottomLeft: mapToItem(target.parent, leftmost, bottommost),
118+ bottomRight: mapToItem(target.parent, rightmost, bottommost)
99119 }
100120 }
101121 }
@@ -113,8 +133,6 @@ Item {
113133 UbuntuNumberAnimation {}
114134 }
115135
116− readonly property bool anyMaximized: target && (target.maximized || target.maximizedLeft || target.maximizedRight)
117−
118136 Image {
119137 source: "graphics/arrows-centre.png"
120138 width: units.gu(10)
@@ -125,15 +143,33 @@ Item {
125143
126144 // move handler
127145 MouseArea {
128− id: moveHandler
146+ id: mouseArea
129147 anchors.fill: parent
130148 visible: overlay.visible
131149 enabled: visible
132150 hoverEnabled: true
133− cursorShape: priv.dragging ? Qt.ClosedHandCursor : (overlay.visible ? Qt.OpenHandCursor : Qt.ArrowCursor)
134151
135− onPressedChanged: priv.handlePressedChanged(pressed, mouseX, mouseY)
136− onPositionChanged: priv.handlePositionChanged(mouseX, mouseY)
152+ onPressedChanged: moveHandler.handlePressedChanged(pressed, pressedButtons, mouseX, mouseY)
153+ onPositionChanged: moveHandler.handlePositionChanged(mouse)
154+ onReleased: {
155+ root.dragReleased();
156+ moveHandler.handleReleased();
157+ }
158+ }
159+
160+ MoveHandler {
161+ id: moveHandler
162+ objectName: "moveHandler"
163+ target: root.target
164+
165+ onFakeMaximizeAnimationRequested: root.fakeMaximizeAnimationRequested(amount)
166+ onFakeMaximizeLeftAnimationRequested: root.fakeMaximizeLeftAnimationRequested(amount)
167+ onFakeMaximizeRightAnimationRequested: root.fakeMaximizeRightAnimationRequested(amount)
168+ onFakeMaximizeTopLeftAnimationRequested: root.fakeMaximizeTopLeftAnimationRequested(amount)
169+ onFakeMaximizeTopRightAnimationRequested: root.fakeMaximizeTopRightAnimationRequested(amount)
170+ onFakeMaximizeBottomLeftAnimationRequested: root.fakeMaximizeBottomLeftAnimationRequested(amount)
171+ onFakeMaximizeBottomRightAnimationRequested: root.fakeMaximizeBottomRightAnimationRequested(amount)
172+ onStopFakeAnimation: root.stopFakeAnimation()
137173 }
138174
139175 // dismiss area
@@ -148,7 +184,7 @@ Item {
148184 }
149185
150186 overlayTimer.stop();
151− mouse.accepted = root.contains(Qt.point(mouse.x, mouse.y));
187+ mouse.accepted = root.contains(mapToItem(root.target, mouse.x, mouse.y));
152188 }
153189 propagateComposedEvents: true
154190 }
@@ -157,7 +193,7 @@ Item {
157193 ResizeGrip { // top left
158194 anchors.horizontalCenter: parent.left
159195 anchors.verticalCenter: parent.top
160− visible: target && !overlay.anyMaximized && !target.maximizedHorizontally && !target.maximizedVertically
196+ visible: priv.ensureWindow || target.maximizedBottomRight
161197 resizeTarget: priv.resizeArea
162198 }
163199
@@ -165,7 +201,7 @@ Item {
165201 anchors.horizontalCenter: parent.horizontalCenter
166202 anchors.verticalCenter: parent.top
167203 rotation: 45
168− visible: target && !overlay.anyMaximized && !target.maximizedVertically
204+ visible: priv.ensureWindow || target.maximizedHorizontally || target.maximizedBottomLeft || target.maximizedBottomRight
169205 resizeTarget: priv.resizeArea
170206 }
171207
@@ -173,7 +209,7 @@ Item {
173209 anchors.horizontalCenter: parent.right
174210 anchors.verticalCenter: parent.top
175211 rotation: 90
176− visible: target && !overlay.anyMaximized && !target.maximizedHorizontally && !target.maximizedVertically
212+ visible: priv.ensureWindow || target.maximizedBottomLeft
177213 resizeTarget: priv.resizeArea
178214 }
179215
@@ -181,14 +217,15 @@ Item {
181217 anchors.horizontalCenter: parent.right
182218 anchors.verticalCenter: parent.verticalCenter
183219 rotation: 135
184− visible: target && !target.maximizedRight && !target.maximized && !target.maximizedHorizontally
220+ visible: priv.ensureWindow || target.maximizedVertically || target.maximizedLeft ||
221+ target.maximizedTopLeft || target.maximizedBottomLeft
185222 resizeTarget: priv.resizeArea
186223 }
187224
188225 ResizeGrip { // bottom right
189226 anchors.horizontalCenter: parent.right
190227 anchors.verticalCenter: parent.bottom
191− visible: target && !overlay.anyMaximized && !target.maximizedHorizontally && !target.maximizedVertically
228+ visible: priv.ensureWindow || target.maximizedTopLeft
192229 resizeTarget: priv.resizeArea
193230 }
194231
@@ -196,7 +233,7 @@ Item {
196233 anchors.horizontalCenter: parent.horizontalCenter
197234 anchors.verticalCenter: parent.bottom
198235 rotation: 45
199− visible: target && !overlay.anyMaximized && !target.maximizedVertically
236+ visible: priv.ensureWindow || target.maximizedHorizontally || target.maximizedTopLeft || target.maximizedTopRight
200237 resizeTarget: priv.resizeArea
201238 }
202239
@@ -204,7 +241,7 @@ Item {
204241 anchors.horizontalCenter: parent.left
205242 anchors.verticalCenter: parent.bottom
206243 rotation: 90
207− visible: target && !overlay.anyMaximized && !target.maximizedHorizontally && !target.maximizedVertically
244+ visible: priv.ensureWindow || target.maximizedTopRight
208245 resizeTarget: priv.resizeArea
209246 }
210247
@@ -212,7 +249,8 @@ Item {
212249 anchors.horizontalCenter: parent.left
213250 anchors.verticalCenter: parent.verticalCenter
214251 rotation: 135
215− visible: target && !target.maximizedLeft && !target.maximized && !target.maximizedHorizontally
252+ visible: priv.ensureWindow || target.maximizedVertically || target.maximizedRight ||
253+ target.maximizedTopRight || target.maximizedBottomRight
216254 resizeTarget: priv.resizeArea
217255 }
218256 }
qml/Stages/WindowDecoration.qml+10−38
@@ -15,22 +15,24 @@
1515 */
1616
1717import QtQuick 2.4
18−import Unity.Application 0.1 // For Mir singleton
1918import Ubuntu.Components 1.3
2019import "../Components"
21−import "../Components/PanelState"
2220
2321MouseArea {
2422 id: root
2523 clip: true
2624
27− property Item target
25+ property Item target // appDelegate
2826 property alias title: titleLabel.text
2927 property alias maximizeButtonShown: buttons.maximizeButtonShown
3028 property bool active: false
31− acceptedButtons: Qt.AllButtons // prevent leaking unhandled mouse events
3229 property alias overlayShown: buttons.overlayShown
3330
31+ readonly property real buttonsWidth: buttons.width + row.spacing
32+
33+ acceptedButtons: Qt.AllButtons // prevent leaking unhandled mouse events
34+ hoverEnabled: true
35+
3436 signal closeClicked()
3537 signal minimizeClicked()
3638 signal maximizeClicked()
@@ -38,42 +40,11 @@ MouseArea {
3840 signal maximizeVerticallyClicked()
3941
4042 onDoubleClicked: {
41− if (maximizeButtonShown && mouse.button == Qt.LeftButton) {
43+ if (target.canBeMaximized && mouse.button == Qt.LeftButton) {
4244 root.maximizeClicked();
4345 }
4446 }
4547
46− QtObject {
47− id: priv
48− property real distanceX
49− property real distanceY
50− property bool dragging
51− }
52−
53− onPressedChanged: {
54− if (pressed && pressedButtons == Qt.LeftButton) {
55− var pos = mapToItem(root.target, mouseX, mouseY);
56− priv.distanceX = pos.x;
57− priv.distanceY = pos.y;
58− priv.dragging = true;
59− } else {
60− priv.dragging = false;
61− Mir.cursorName = "";
62− }
63− }
64−
65− onPositionChanged: {
66− if (priv.dragging) {
67− Mir.cursorName = "grabbing";
68− var pos = mapToItem(root.target.parent, mouseX, mouseY);
69− // Use integer coordinate values to ensure that target is left in a pixel-aligned
70− // position. Mouse movement could have subpixel precision, yielding a fractional
71− // mouse position.
72− root.target.requestedX = Math.round(pos.x - priv.distanceX);
73− root.target.requestedY = Math.round(Math.max(pos.y - priv.distanceY, PanelState.panelHeight));
74− }
75− }
76−
7748 // do not let unhandled wheel event pass thru the decoration
7849 onWheel: wheel.accepted = true;
7950
@@ -85,6 +56,7 @@ MouseArea {
8556 }
8657
8758 Row {
59+ id: row
8860 anchors {
8961 fill: parent
9062 leftMargin: overlayShown ? units.gu(5) : units.gu(1)
@@ -105,8 +77,8 @@ MouseArea {
10577 onCloseClicked: root.closeClicked();
10678 onMinimizeClicked: root.minimizeClicked();
10779 onMaximizeClicked: root.maximizeClicked();
108− onMaximizeHorizontallyClicked: root.maximizeHorizontallyClicked();
109− onMaximizeVerticallyClicked: root.maximizeVerticallyClicked();
80+ onMaximizeHorizontallyClicked: if (root.target.canBeMaximizedHorizontally) root.maximizeHorizontallyClicked();
81+ onMaximizeVerticallyClicked: if (root.target.canBeMaximizedVertically) root.maximizeVerticallyClicked();
11082 closeButtonShown: root.target.application.appId !== "unity8-dash"
11183 }
11284
qml/Stages/WindowResizeArea.qml+13−24
@@ -30,6 +30,8 @@ MouseArea {
3030
3131 property var windowStateStorage: WindowStateStorage
3232 readonly property alias dragging: d.dragging
33+ readonly property alias normalWidth: priv.normalWidth
34+ readonly property alias normalHeight: priv.normalHeight
3335
3436 // The target item managed by this. Must be a parent or a sibling
3537 // The area will anchor to it and manage resize events
@@ -54,7 +56,7 @@ MouseArea {
5456 property int normalHeight: 0
5557
5658 function updateNormalGeometry() {
57− if (root.target.state == "normal") {
59+ if (root.target.state == "normal" || root.target.state == "restored") {
5860 normalX = root.target.requestedX
5961 normalY = root.target.requestedY
6062 normalWidth = root.target.width
@@ -83,34 +85,21 @@ MouseArea {
8385 target.requestedY = Qt.binding(function() { return Math.max(Math.min(windowGeometry.y, root.screenHeight - target.requestedHeight), PanelState.panelHeight); });
8486
8587 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− }
88+ target.restore(false /* animated */, windowState);
10889
10990 priv.updateNormalGeometry();
91+
92+ // initialize the x/y to restore to
93+ target.restoredX = priv.normalX;
94+ target.restoredY = priv.normalY;
11095 }
11196
11297 function saveWindowState() {
113− windowStateStorage.saveState(root.windowId, target.windowState & ~WindowStateStorage.WindowStateMinimized); // clear the minimized bit when saving
98+ var state = target.windowState;
99+ if (state === WindowStateStorage.WindowStateRestored) {
100+ state = WindowStateStorage.WindowStateNormal;
101+ }
102+ windowStateStorage.saveState(root.windowId, state & ~WindowStateStorage.WindowStateMinimized); // clear the minimized bit when saving
114103 windowStateStorage.saveGeometry(root.windowId, Qt.rect(priv.normalX, priv.normalY, priv.normalWidth, priv.normalHeight));
115104 }
116105
tests/mocks/Cursor/CMakeLists.txt+1−1
@@ -1 +1 @@
1−add_unity8_mock(Cursor 1.0 Cursor PREFIX mocks)
1+add_unity8_mock(Cursor 1.1 Cursor PREFIX mocks)
tests/mocks/Cursor/Cursor.qml+8
@@ -17,8 +17,16 @@
1717import QtQuick 2.4
1818
1919Item {
20+ property int topBoundaryOffset // effectively panel height
2021 property Item confiningItem
22+
2123 signal pushedLeftBoundary(real amount, int buttons)
2224 signal pushedRightBoundary(real amount, int buttons)
25+ signal pushedTopBoundary(real amount, int buttons)
26+ signal pushedTopLeftCorner(real amount, int buttons)
27+ signal pushedTopRightCorner(real amount, int buttons)
28+ signal pushedBottomLeftCorner(real amount, int buttons)
29+ signal pushedBottomRightCorner(real amount, int buttons)
30+ signal pushStopped()
2331 signal mouseMoved()
2432}
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/Panel/tst_Panel.qml−2
@@ -388,8 +388,6 @@ IndicatorTest {
388388 tap(panel, stepLength * i, tapY);
389389 tryCompare(panel.indicators, "fullyClosed", true);
390390 }
391−
392− compare(backgroundPressedSpy.count, 0);
393391 }
394392
395393 function test_darkenedAreaEatsAllEvents() {
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 {
tests/qmltests/tst_Shell.qml+1−4
@@ -2664,10 +2664,7 @@ Rectangle {
26642664 appDelegate.maximize();
26652665 tryCompare(appDelegate, "state", "maximized");
26662666
2667− mousePress(panel);
2668− mouseMove(shell, shell.width/2, shell.height/2);
2669− mouseRelease(shell);
2670−
2667+ mouseDrag(panel, panel.width/3, panel.height/2, 0, shell.height/3, Qt.LeftButton, Qt.NoModifier, 500);
26712668 tryCompare(appDelegate, "state", "normal");
26722669 }
26732670