on xenial help sign in
Merge remote-tracking branch 'screens-workspaces-switcher' into xenial_-_edge_-_multiscreen

This merges: https://code.launchpad.net/~unity-team/unity8/screens-workspaces-switcher/+merge/320155

commite961d7c720db1418f8045ef7b3c155f96076cb32 authorMarius Gripsgard <marius@ubports.com> dateWed 3 Jul 2019, 01:04 (7 years ago) parent61e58b7de450aa
145 files changed+6618−1533
CMakeLists.txt+5−1 debian/control+6 debian/unity8.install+5 plugins/Cursor/CMakeLists.txt+1 plugins/Cursor/InputDispatcherFilter.cpp+175 plugins/Cursor/InputDispatcherFilter.h+62 plugins/Cursor/MousePointer.cpp+63−78 plugins/Cursor/MousePointer.h+5−4 plugins/GlobalShortcut/globalshortcut.cpp−15 plugins/GlobalShortcut/globalshortcut.h−4 plugins/GlobalShortcut/globalshortcutregistry.cpp+23−15 plugins/GlobalShortcut/globalshortcutregistry.h+2−2 plugins/UInput/plugin.cpp+7−1 plugins/WindowManager/CMakeLists.txt+28−7 plugins/WindowManager/Screen.cpp+316 plugins/WindowManager/Screen.h+153 plugins/WindowManager/ScreenAttached.cpp+164 plugins/WindowManager/ScreenAttached.h+56 plugins/WindowManager/ScreenWindow.cpp+45 plugins/WindowManager/ScreenWindow.h+49 plugins/WindowManager/Screens.cpp+245 plugins/WindowManager/Screens.h+114 plugins/WindowManager/ScreensConfiguration.cpp+94 src/ShellView.h → plugins/WindowManager/ScreensConfiguration.h+10−12 plugins/WindowManager/TopLevelWindowModel.cpp+207−75 plugins/WindowManager/TopLevelWindowModel.h+19−24 plugins/WindowManager/WindowManagerObjects.cpp+47 plugins/WindowManager/WindowManagerObjects.h+66 plugins/WindowManager/WindowManagerPlugin.cpp+57−2 plugins/WindowManager/WindowManagerPlugin.h+1 plugins/WindowManager/Workspace.cpp+178 plugins/WindowManager/Workspace.h+122 plugins/WindowManager/WorkspaceManager.cpp+106 plugins/WindowManager/WorkspaceManager.h+69 plugins/WindowManager/WorkspaceModel.cpp+254 plugins/WindowManager/WorkspaceModel.h+102 qml/ApplicationMenus/ApplicationMenuItemFactory.qml+4−4 qml/ApplicationMenus/MenuBar.qml+5−1 qml/ApplicationMenus/MenuPopup.qml+3−1 qml/CMakeLists.txt+1−1 qml/Components/PanelState/PanelState.qml−1 qml/Components/PanelState/qmldir+1−1 qml/Components/VirtualTouchPad.qml+14−20 qml/DisabledScreenNotice.qml+2−1 qml/ErrorApplication.qml+67 qml/Greeter/Greeter.qml+21−3 qml/Greeter/SecondaryGreeter.qml+75 qml/OrientedShell.qml+5−8 qml/Panel/Panel.qml+18−16 qml/Shell.qml+41−25 qml/ShellApplication.qml+60 qml/ShellNotifier.qml+30 qml/ShellScreen.qml+64 qml/Stage/ApplicationWindow.qml+10−207 qml/Stage/DecoratedWindow.qml+13−11 qml/Stage/FakeMaximizeDelegate.qml+12−11 qml/Stage/Spread/ScreensAndWorkspaces.qml+261 qml/Stage/Spread/Spread.qml+7−8 qml/Stage/Spread/SpreadDelegateInputArea.qml+36−17 qml/Stage/Spread/WorkspacePreview.qml+127 qml/Stage/Spread/Workspaces.qml+412 qml/Stage/Stage.qml+223−112 qml/Stage/WindowDecoration.qml+2 qml/Stage/WindowResizeArea.qml+27−4 qml/Stage/WindowStateSaver.qml+4−1 qml/Stage/WorkspaceSwitcher.qml+196 qml/Stage/graphics/multi-monitor_drop-here.png qml/Stage/graphics/multi-monitor_leave.png qml/qmldir+1 src/ApplicationArguments.cpp+11−2 src/ApplicationArguments.h+17−15 src/CMakeLists.txt+12−4 src/DisplayConfigurationStorage.cpp+90 src/DisplayConfigurationStorage.h+15 src/SecondaryWindow.cpp−40 src/ShellApplication.cpp−221 src/ShellView.cpp−85 src/UnityApplication.cpp+152 src/ShellApplication.h → src/UnityApplication.h+9−19 src/UnityCommandLineParser.cpp+8 src/UnityCommandLineParser.h+4−1 src/WindowManagementPolicy.cpp+69 src/WindowManagementPolicy.h+47 src/libunity8-private/CMakeLists.txt+1 src/SecondaryWindow.h → src/libunity8-private/wmpolicyinterface.cpp+6−12 src/libunity8-private/wmpolicyinterface.h+46 src/main.cpp+4−8 tests/CMakeLists.txt+1 tests/mocks/CMakeLists.txt+1 tests/mocks/Cursor/Cursor.qml+49−2 tests/mocks/GSettings.1.0/plugin.cpp+12 tests/mocks/GSettings.1.0/plugin.h+1 tests/mocks/UInput/plugin.cpp+6−1 tests/mocks/Unity/Application/CMakeLists.txt+26−1 tests/mocks/Unity/Application/MirSurfaceItem.cpp+26−7 tests/mocks/Unity/Application/SurfaceManager.cpp+118−11 tests/mocks/Unity/Application/SurfaceManager.h+23−3 tests/mocks/Unity/Application/plugin.cpp+7−1 tests/mocks/Unity/Application/resources/MirSurfaceItem.qml−3 tests/mocks/Unity/CMakeLists.txt−1 tests/mocks/Unity/InputInfo/mockcontroller.cpp+10 tests/mocks/Unity/InputInfo/mockcontroller.h+3−1 tests/mocks/Unity/InputInfo/plugin.cpp+1−2 tests/mocks/Unity/Screens/CMakeLists.txt−14 tests/mocks/Unity/Screens/qmldir−2 tests/mocks/Unity/Screens/screens.cpp−71 tests/mocks/Unity/Screens/screens.h−82 tests/mocks/WindowManager/CMakeLists.txt+61 tests/mocks/Unity/Screens/plugin.cpp → tests/mocks/WindowManager/MockScreenWindow.cpp+14−9 tests/mocks/Unity/Screens/plugin.h → tests/mocks/WindowManager/MockScreenWindow.h+11−6 tests/mocks/WindowManager/MockScreens.cpp+230 tests/mocks/WindowManager/MockScreens.h+54 tests/mocks/WindowManager/MockScreensConfiguration.cpp+47 tests/mocks/WindowManager/WindowManagementPolicy.cpp+128 tests/mocks/WindowManager/WindowManagementPolicy.h+75 tests/mocks/WindowManager/WindowManagerPlugin.cpp+89 tests/mocks/WindowManager/WindowManagerPlugin.h+33 tests/mocks/WindowManager/qmldir+2 tests/plugins/GlobalShortcut/GlobalShortcutTest.cpp+1 tests/plugins/WindowManager/CMakeLists.txt+5−1 tests/plugins/WindowManager/UnityApplicationMocks.h+25 tests/plugins/WindowManager/tst_TopLevelWindowModel.cpp+20−8 tests/qmltests/ApplicationMenuDataLoader.qml+3−6 tests/qmltests/ApplicationMenus/tst_MenuBar.qml+2−2 tests/qmltests/ApplicationMenus/tst_MenuPopup.qml+2 tests/qmltests/CMakeLists.txt+1 tests/qmltests/Components/tst_VirtualTouchPad.qml+2−2 tests/qmltests/Panel/tst_ActiveCallHint.qml−2 tests/qmltests/Panel/tst_Panel.qml+39−15 tests/qmltests/Stage/tst_ApplicationWindow.qml+10−44 tests/qmltests/Stage/tst_DecoratedWindow.qml+5−3 tests/qmltests/Stage/tst_DesktopStage.qml+7−13 tests/qmltests/Stage/tst_PhoneStage.qml+7−8 tests/qmltests/Stage/tst_SurfaceContainer.qml−2 tests/qmltests/Stage/tst_TabletStage.qml+65−22 tests/qmltests/Stage/tst_WindowDecoration.qml−2 tests/qmltests/Stage/tst_WindowResizeArea.qml+1−8 tests/qmltests/Tutorial/tst_Tutorial.qml+5−23 tests/qmltests/tst_OrientedShell.qml+22−10 tests/qmltests/tst_Shell.qml+40−28 tests/qmltests/tst_ShellApplication.qml+218 tests/qmltests/tst_ShellWithPin.qml+4−16 tests/uqmlscene/main.cpp+2 tests/utils/modules/Unity/Test/StageTestCase.qml+1−1 tests/utils/modules/Unity/Test/UnityTestCase.qml+14
CMakeLists.txt+5−1
@@ -72,6 +72,7 @@ find_package(Qt5Sql 5.6 REQUIRED)
7272find_package(Qt5Test 5.6 REQUIRED)
7373
7474pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=27)
75+pkg_check_modules(QTMIRSERVER REQUIRED qtmirserver>=0.6.0)
7576pkg_check_modules(GEONAMES REQUIRED geonames>=0.2)
7677pkg_check_modules(GIO REQUIRED gio-2.0>=2.32)
7778pkg_check_modules(GLIB REQUIRED glib-2.0>=2.32)
@@ -84,6 +85,7 @@ pkg_check_modules(QTDBUSTEST REQUIRED
8485 libqtdbustest-1
8586 libqtdbusmock-1
8687 )
88+pkg_check_modules(MIRAL REQUIRED miral)
8789
8890### Check UbuntuGestures private headers. No pkg-config (.pc) file is provided for them
8991find_path(UBUNTUGESTUREPRIV
@@ -118,7 +120,9 @@ if(SHELL_PLUGINDIR STREQUAL "")
118120 message(FATAL_ERROR "Could not determine plugin import dir.")
119121endif()
120122
121−set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -fno-permissive -pedantic -Wall -Wextra")
123+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-permissive -pedantic -Wall -Wextra")
124+
125+set (CMAKE_CXX_STANDARD 14)
122126
123127if ("${CMAKE_BUILD_TYPE}" STREQUAL "release" OR "${CMAKE_BUILD_TYPE}" STREQUAL "relwithdebinfo")
124128 option(Werror "Treat warnings as errors" ON)
debian/control+6
@@ -33,6 +33,7 @@ Build-Depends: cmake,
3333 libqtdbustest1-dev,
3434 libqt5svg5-dev,
3535 libqt5xmlpatterns5-dev,
36+ libqtmirserver-dev (>= 0.6.0),
3637 libsystemsettings-dev,
3738 libubuntu-app-launch2-dev,
3839 libubuntu-download-manager-common-dev,
@@ -47,6 +48,7 @@ Build-Depends: cmake,
4748 libxcb1-dev[!arm64 !armhf],
4849 libxi-dev[!arm64 !armhf],
4950# End of X11 libs
51+ mirtest-dev,
5052 pkg-config,
5153 python3-all:any,
5254 python3-setuptools,
@@ -75,6 +77,9 @@ Build-Depends: cmake,
7577 ttf-ubuntu-font-family,
7678 xvfb,
7779 android-headers,
80+# mirtest pkgconfig requires these, but doesn't have a deb dependency. Bug lp:1633537
81+ libboost-filesystem-dev,
82+ libboost-system-dev,
7883Standards-Version: 3.9.4
7984Homepage: http://launchpad.net/unity
8085# If you aren't a member of ~unity-team but need to upload
@@ -123,6 +128,7 @@ Depends: dmz-cursor-theme,
123128# qml-module-ubuntu-web,
124129 qtdeclarative5-qtmir-plugin (>= 0.4.8),
125130 qtdeclarative5-ubuntu-telephony0.1,
131+ qtmir-desktop (>= 0.6.0) | qtmir-android (>= 0.6.0),
126132 ubuntu-system-settings (>= 0.4),
127133 unity-launcher-impl-13,
128134 unity8-common (= ${source:Version}),
debian/unity8.install+5
@@ -13,7 +13,12 @@ usr/share/unity8/Rotation
1313usr/share/unity8/DeviceConfiguration.qml
1414usr/share/unity8/OrientedShell.qml
1515usr/share/unity8/DisabledScreenNotice.qml
16+usr/share/unity8/ErrorApplication.qml
17+usr/share/unity8/qmldir
1618usr/share/unity8/Shell.qml
19+usr/share/unity8/ShellApplication.qml
20+usr/share/unity8/ShellNotifier.qml
21+usr/share/unity8/ShellScreen.qml
1722usr/share/unity8/Stage
1823usr/share/unity8/Tutorial
1924usr/share/unity8/Wizard
plugins/Cursor/CMakeLists.txt+1
@@ -15,6 +15,7 @@ set(QMLPLUGIN_SRC
1515 CursorImageInfo.cpp
1616 CursorImageProvider.cpp
1717 MousePointer.cpp
18+ InputDispatcherFilter.cpp
1819 # We need to run moc on this header
1920 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirMousePointerInterface.h
2021 )
plugins/Cursor/InputDispatcherFilter.cppnew file+175
@@ -0,0 +1,175 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "InputDispatcherFilter.h"
18+#include "MousePointer.h"
19+
20+#include <QEvent>
21+#include <QGuiApplication>
22+#include <QQuickWindow>
23+#include <QScreen>
24+#include <QtMath>
25+#include <qpa/qplatformnativeinterface.h>
26+#include <qpa/qplatformscreen.h>
27+
28+InputDispatcherFilter *InputDispatcherFilter::instance()
29+{
30+ static InputDispatcherFilter filter;
31+ return &filter;
32+}
33+
34+InputDispatcherFilter::InputDispatcherFilter(QObject *parent)
35+ : QObject(parent)
36+ , m_pushing(false)
37+{
38+ QPlatformNativeInterface *ni = QGuiApplication::platformNativeInterface();
39+ m_inputDispatcher = static_cast<QObject*>(ni->nativeResourceForIntegration("InputDispatcher"));
40+ if (m_inputDispatcher) {
41+ m_inputDispatcher->installEventFilter(this);
42+ }
43+}
44+
45+void InputDispatcherFilter::registerPointer(MousePointer *pointer)
46+{
47+ // allow first registered pointer to be visible.
48+ m_pointers.insert(pointer);
49+}
50+
51+void InputDispatcherFilter::unregisterPointer(MousePointer *pointer)
52+{
53+ m_pointers.remove(pointer);
54+}
55+
56+bool InputDispatcherFilter::eventFilter(QObject *o, QEvent *e)
57+{
58+ if (o != m_inputDispatcher) return false;
59+
60+ switch (e->type()) {
61+ case QEvent::MouseMove:
62+ case QEvent::MouseButtonPress:
63+ case QEvent::MouseButtonRelease:
64+ {
65+ // if we don't have any pointers, filter all mouse events.
66+ auto pointer = currentPointer();
67+ if (!pointer) return true;
68+
69+ QMouseEvent* me = static_cast<QMouseEvent*>(e);
70+
71+ // Local position gives relative change of mouse pointer.
72+ QPointF movement = me->localPos();
73+
74+ // Adjust the position
75+ QPointF oldPos = pointer->window()->geometry().topLeft() + pointer->position();
76+ QPointF newPos = adjustedPositionForMovement(oldPos, movement);
77+
78+ QScreen* currentScreen = screenAt(newPos);
79+ if (currentScreen) {
80+ QRect screenRect = currentScreen->geometry();
81+ qreal newX = (oldPos + movement).x();
82+ qreal newY = (oldPos + movement).y();
83+
84+ if (newX <= screenRect.left() && newY < screenRect.top() + pointer->topBoundaryOffset()) { // top left corner
85+ const auto distance = qSqrt(qPow(newX, 2) + qPow(newY- screenRect.top() - pointer->topBoundaryOffset(), 2));
86+ Q_EMIT pushedTopLeftCorner(currentScreen, qAbs(distance), me->buttons());
87+ m_pushing = true;
88+ } else if (newX >= screenRect.right()-1 && newY < screenRect.top() + pointer->topBoundaryOffset()) { // top right corner
89+ const auto distance = qSqrt(qPow(newX-screenRect.right(), 2) + qPow(newY - screenRect.top() - pointer->topBoundaryOffset(), 2));
90+ Q_EMIT pushedTopRightCorner(currentScreen, qAbs(distance), me->buttons());
91+ m_pushing = true;
92+ } else if (newX < 0 && newY >= screenRect.bottom()-1) { // bottom left corner
93+ const auto distance = qSqrt(qPow(newX, 2) + qPow(newY-screenRect.bottom(), 2));
94+ Q_EMIT pushedBottomLeftCorner(currentScreen, qAbs(distance), me->buttons());
95+ m_pushing = true;
96+ } else if (newX >= screenRect.right()-1 && newY >= screenRect.bottom()-1) { // bottom right corner
97+ const auto distance = qSqrt(qPow(newX-screenRect.right(), 2) + qPow(newY-screenRect.bottom(), 2));
98+ Q_EMIT pushedBottomRightCorner(currentScreen, qAbs(distance), me->buttons());
99+ m_pushing = true;
100+ } else if (newX < screenRect.left()) { // left edge
101+ Q_EMIT pushedLeftBoundary(currentScreen, qAbs(newX), me->buttons());
102+ m_pushing = true;
103+ } else if (newX >= screenRect.right()) { // right edge
104+ Q_EMIT pushedRightBoundary(currentScreen, newX - (screenRect.right() - 1), me->buttons());
105+ m_pushing = true;
106+ } else if (newY < screenRect.top() + pointer->topBoundaryOffset()) { // top edge
107+ Q_EMIT pushedTopBoundary(currentScreen, qAbs(newY - screenRect.top() - pointer->topBoundaryOffset()), me->buttons());
108+ m_pushing = true;
109+ } else if (Q_LIKELY(newX > 0 && newX < screenRect.right()-1 && newY > 0 && newY < screenRect.bottom()-1)) { // normal pos, not pushing
110+ if (m_pushing) {
111+ Q_EMIT pushStopped(currentScreen);
112+ m_pushing = false;
113+ }
114+ }
115+ }
116+
117+ // Send the event
118+ QMouseEvent eCopy(me->type(), me->localPos(), newPos, me->button(), me->buttons(), me->modifiers());
119+ eCopy.setTimestamp(me->timestamp());
120+ o->event(&eCopy);
121+ return true;
122+ }
123+ default:
124+ break;
125+ }
126+ return false;
127+}
128+
129+QPointF InputDispatcherFilter::adjustedPositionForMovement(const QPointF &pt, const QPointF &movement) const
130+{
131+ QPointF adjusted = pt + movement;
132+
133+ auto screen = screenAt(adjusted); // first check if our move was to a screen with an enabled pointer.
134+ if (screen) {
135+ return adjusted;
136+ } else if ((screen = screenAt(pt))) { // then check if our old position was valid
137+ QRectF screenBounds = screen->geometry();
138+ // bound the new position to the old screen geometry
139+ adjusted.rx() = qMax(screenBounds.left(), qMin(adjusted.x(), screenBounds.right()-1));
140+ adjusted.ry() = qMax(screenBounds.top(), qMin(adjusted.y(), screenBounds.bottom()-1));
141+ } else {
142+ auto screens = QGuiApplication::screens();
143+
144+ // center of first screen with a pointer.
145+ Q_FOREACH(QScreen* screen, screens) {
146+ Q_FOREACH(MousePointer* pointer, m_pointers) {
147+ if (pointer->isEnabled() && pointer->screen() == screen) {
148+ return screen->geometry().center();
149+ }
150+ }
151+ }
152+ }
153+ return adjusted;
154+}
155+
156+QScreen *InputDispatcherFilter::screenAt(const QPointF &pt) const
157+{
158+ Q_FOREACH(MousePointer* pointer, m_pointers) {
159+ if (!pointer->isEnabled()) continue;
160+
161+ QScreen* screen = pointer->screen();
162+ if (screen && screen->geometry().contains(pt.toPoint()))
163+ return screen;
164+ }
165+ return nullptr;
166+}
167+
168+MousePointer *InputDispatcherFilter::currentPointer() const
169+{
170+ Q_FOREACH(MousePointer* pointer, m_pointers) {
171+ if (!pointer->isEnabled()) continue;
172+ return pointer;
173+ }
174+ return nullptr;
175+}
plugins/Cursor/InputDispatcherFilter.hnew file+62
@@ -0,0 +1,62 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef INPUTDISPATCHERFILTER_H
18+#define INPUTDISPATCHERFILTER_H
19+
20+#include <QObject>
21+#include <QPointF>
22+#include <QSet>
23+
24+class MousePointer;
25+class QScreen;
26+
27+class InputDispatcherFilter : public QObject
28+{
29+ Q_OBJECT
30+public:
31+ static InputDispatcherFilter *instance();
32+
33+ void registerPointer(MousePointer* pointer);
34+ void unregisterPointer(MousePointer* pointer);
35+
36+Q_SIGNALS:
37+ void pushedLeftBoundary(QScreen* screen, qreal amount, Qt::MouseButtons buttons);
38+ void pushedRightBoundary(QScreen* screen, qreal amount, Qt::MouseButtons buttons);
39+ void pushedTopBoundary(QScreen* screen, qreal amount, Qt::MouseButtons buttons);
40+ void pushedTopLeftCorner(QScreen* screen, qreal amount, Qt::MouseButtons buttons);
41+ void pushedTopRightCorner(QScreen* screen, qreal amount, Qt::MouseButtons buttons);
42+ void pushedBottomLeftCorner(QScreen* screen, qreal amount, Qt::MouseButtons buttons);
43+ void pushedBottomRightCorner(QScreen* screen, qreal amount, Qt::MouseButtons buttons);
44+ void pushStopped(QScreen* screen);
45+
46+protected:
47+ InputDispatcherFilter(QObject* parent = nullptr);
48+
49+ bool eventFilter(QObject *o, QEvent *e) override;
50+
51+ QPointF adjustedPositionForMovement(const QPointF& pt, const QPointF& movement) const;
52+ QScreen* screenAt(const QPointF& pt) const;
53+
54+ MousePointer* currentPointer() const;
55+
56+private:
57+ QObject* m_inputDispatcher;
58+ QSet<MousePointer*> m_pointers;
59+ bool m_pushing;
60+};
61+
62+#endif // INPUTDISPATCHERFILTER_H
plugins/Cursor/MousePointer.cpp+63−78
@@ -16,84 +16,74 @@
1616
1717#include "MousePointer.h"
1818#include "CursorImageProvider.h"
19−
20−// Unity API
21−#include <unity/shell/application/MirPlatformCursor.h>
19+#include "InputDispatcherFilter.h"
2220
2321#include <QQuickWindow>
24−#include <QGuiApplication>
25−#include <QtMath>
2622
27−#include <qpa/qwindowsysteminterface.h>
23+// Unity API
24+#include <unity/shell/application/MirPlatformCursor.h>
2825
2926MousePointer::MousePointer(QQuickItem *parent)
3027 : MirMousePointerInterface(parent)
3128 , m_cursorName(QStringLiteral("left_ptr"))
3229 , m_themeName(QStringLiteral("default"))
3330{
34−}
35−
36−void MousePointer::handleMouseEvent(ulong timestamp, QPointF movement, Qt::MouseButtons buttons,
37− Qt::KeyboardModifiers modifiers)
38−{
39− if (!parentItem()) {
40− return;
41− }
42−
43− if (!movement.isNull()) {
44− Q_EMIT mouseMoved();
45− }
46−
47− m_accumulatedMovement += movement;
48− // don't apply the fractional part
49− QPointF appliedMovement(int(m_accumulatedMovement.x()), int(m_accumulatedMovement.y()));
50− m_accumulatedMovement -= appliedMovement;
51−
52− qreal newX = x() + appliedMovement.x();
53− qreal newY = y() + appliedMovement.y();
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) {
31+ connect(InputDispatcherFilter::instance(), &InputDispatcherFilter::pushedLeftBoundary,
32+ this, [this](QScreen* screen, qreal amount, Qt::MouseButtons buttons) {
33+ if (window() && window()->screen() == screen) {
34+ Q_EMIT pushedLeftBoundary(amount, buttons);
35+ }
36+ });
37+ connect(InputDispatcherFilter::instance(), &InputDispatcherFilter::pushedRightBoundary,
38+ this, [this](QScreen* screen, qreal amount, Qt::MouseButtons buttons) {
39+ if (window() && window()->screen() == screen) {
40+ Q_EMIT pushedRightBoundary(amount, buttons);
41+ }
42+ });
43+ connect(InputDispatcherFilter::instance(), &InputDispatcherFilter::pushedTopBoundary,
44+ this, [this](QScreen* screen, qreal amount, Qt::MouseButtons buttons) {
45+ if (window() && window()->screen() == screen) {
46+ Q_EMIT pushedTopBoundary(amount, buttons);
47+ }
48+ });
49+ connect(InputDispatcherFilter::instance(), &InputDispatcherFilter::pushedTopLeftCorner,
50+ this, [this](QScreen* screen, qreal amount, Qt::MouseButtons buttons) {
51+ if (window() && window()->screen() == screen) {
52+ Q_EMIT pushedTopLeftCorner(amount, buttons);
53+ }
54+ });
55+ connect(InputDispatcherFilter::instance(), &InputDispatcherFilter::pushedTopRightCorner,
56+ this, [this](QScreen* screen, qreal amount, Qt::MouseButtons buttons) {
57+ if (window() && window()->screen() == screen) {
58+ Q_EMIT pushedTopRightCorner(amount, buttons);
59+ }
60+ });
61+ connect(InputDispatcherFilter::instance(), &InputDispatcherFilter::pushedBottomLeftCorner,
62+ this, [this](QScreen* screen, qreal amount, Qt::MouseButtons buttons) {
63+ if (window() && window()->screen() == screen) {
64+ Q_EMIT pushedBottomLeftCorner(amount, buttons);
65+ }
66+ });
67+ connect(InputDispatcherFilter::instance(), &InputDispatcherFilter::pushedBottomRightCorner,
68+ this, [this](QScreen* screen, qreal amount, Qt::MouseButtons buttons) {
69+ if (window() && window()->screen() == screen) {
70+ Q_EMIT pushedBottomRightCorner(amount, buttons);
71+ }
72+ });
73+ connect(InputDispatcherFilter::instance(), &InputDispatcherFilter::pushStopped,
74+ this, [this](QScreen* screen) {
75+ if (window() && window()->screen() == screen) {
8476 Q_EMIT pushStopped();
85− m_pushing = false;
8677 }
87− }
78+ });
8879
89− applyItemConfinement(newX, newY);
90−
91− setX(qBound(0.0, newX, sceneWidth - 1));
92− setY(qBound(0.0, newY, sceneHeight - 1));
80+ InputDispatcherFilter::instance()->registerPointer(this);
81+}
9382
94− QPointF scenePosition = mapToItem(nullptr, QPointF(0, 0));
95− QWindowSystemInterface::handleMouseEvent(window(), timestamp, scenePosition /*local*/, scenePosition /*global*/,
96− buttons, modifiers);
83+MousePointer::~MousePointer()
84+{
85+ registerScreen(nullptr);
86+ InputDispatcherFilter::instance()->unregisterPointer(this);
9787}
9888
9989void MousePointer::applyItemConfinement(qreal &newX, qreal &newY)
@@ -121,17 +111,6 @@ void MousePointer::applyItemConfinement(qreal &newX, qreal &newY)
121111 }
122112}
123113
124−void MousePointer::handleWheelEvent(ulong timestamp, QPoint angleDelta, Qt::KeyboardModifiers modifiers)
125−{
126− if (!parentItem()) {
127− return;
128− }
129−
130− QPointF scenePosition = mapToItem(nullptr, QPointF(0, 0));
131− QWindowSystemInterface::handleWheelEvent(window(), timestamp, scenePosition /* local */, scenePosition /* global */,
132− QPoint() /* pixelDelta */, angleDelta, modifiers, Qt::ScrollUpdate);
133−}
134−
135114int MousePointer::topBoundaryOffset() const
136115{
137116 return m_topBoundaryOffset;
@@ -182,7 +161,7 @@ void MousePointer::registerScreen(QScreen *screen)
182161 if (m_registeredScreen) {
183162 auto previousCursor = dynamic_cast<MirPlatformCursor*>(m_registeredScreen->handle()->cursor());
184163 if (previousCursor) {
185− previousCursor->setMousePointer(nullptr);
164+ previousCursor->unregisterMousePointer(this);
186165 } else {
187166 qCritical("QPlatformCursor is not a MirPlatformCursor! Cursor module only works in a Mir server.");
188167 }
@@ -193,7 +172,7 @@ void MousePointer::registerScreen(QScreen *screen)
193172 if (m_registeredScreen) {
194173 auto cursor = dynamic_cast<MirPlatformCursor*>(m_registeredScreen->handle()->cursor());
195174 if (cursor) {
196− cursor->setMousePointer(this);
175+ cursor->registerMousePointer(this);
197176 } else {
198177 qCritical("QPlaformCursor is not a MirPlatformCursor! Cursor module only works in Mir.");
199178 }
@@ -216,6 +195,12 @@ void MousePointer::setThemeName(const QString &themeName)
216195 }
217196}
218197
198+void MousePointer::moveTo(const QPoint &position)
199+{
200+ setPosition(position);
201+ Q_EMIT mouseMoved();
202+}
203+
219204void MousePointer::setCustomCursor(const QCursor &customCursor)
220205{
221206 CursorImageProvider::instance()->setCustomCursor(customCursor);
plugins/Cursor/MousePointer.h+5−4
@@ -31,6 +31,7 @@ class MousePointer : public MirMousePointerInterface {
3131 Q_PROPERTY(int topBoundaryOffset READ topBoundaryOffset WRITE setTopBoundaryOffset NOTIFY topBoundaryOffsetChanged)
3232public:
3333 MousePointer(QQuickItem *parent = nullptr);
34+ ~MousePointer();
3435
3536 void setCursorName(const QString &qtCursorName) override;
3637 QString cursorName() const override { return m_cursorName; }
@@ -38,6 +39,8 @@ public:
3839 void setThemeName(const QString &themeName) override;
3940 QString themeName() const override { return m_themeName; }
4041
42+ void moveTo(const QPoint& position) override;
43+
4144 void setCustomCursor(const QCursor &) override;
4245
4346 QQuickItem* confiningItem() const;
@@ -46,10 +49,7 @@ public:
4649 int topBoundaryOffset() const;
4750 void setTopBoundaryOffset(int topBoundaryOffset);
4851
49−public Q_SLOTS:
50− void handleMouseEvent(ulong timestamp, QPointF movement, Qt::MouseButtons buttons,
51− Qt::KeyboardModifiers modifiers) override;
52− void handleWheelEvent(ulong timestamp, QPoint angleDelta, Qt::KeyboardModifiers modifiers) override;
52+ QScreen* screen() const { return m_registeredScreen; }
5353
5454Q_SIGNALS:
5555 void pushedLeftBoundary(qreal amount, Qt::MouseButtons buttons);
@@ -79,6 +79,7 @@ private:
7979 QPointer<QScreen> m_registeredScreen;
8080 QString m_cursorName;
8181 QString m_themeName;
82+ bool m_active;
8283
8384 // Accumulated, unapplied, mouse movement.
8485 QPointF m_accumulatedMovement;
plugins/GlobalShortcut/globalshortcut.cpp−15
@@ -56,11 +56,6 @@ void GlobalShortcut::setActive(bool active)
5656 Q_EMIT activeChanged(active);
5757}
5858
59−void GlobalShortcut::componentComplete()
60−{
61− connect(this, &QQuickItem::windowChanged, this, &GlobalShortcut::setupFilterOnWindow);
62−}
63−
6459void GlobalShortcut::keyPressEvent(QKeyEvent * event)
6560{
6661 if (!m_active) return;
@@ -76,13 +71,3 @@ void GlobalShortcut::keyReleaseEvent(QKeyEvent * event)
7671 event->accept();
7772 Q_EMIT released(m_shortcut.toString());
7873}
79−
80−void GlobalShortcut::setupFilterOnWindow(QQuickWindow *window)
81−{
82− if (!window) {
83−// qWarning() << Q_FUNC_INFO << "Failed to setup filter on window";
84− return;
85− }
86−
87− registry->setupFilterOnWindow((qulonglong) window->winId());
88−}
plugins/GlobalShortcut/globalshortcut.h−4
@@ -53,7 +53,6 @@ public:
5353 void setActive(bool active);
5454
5555protected:
56− void componentComplete() override;
5756 void keyPressEvent(QKeyEvent * event) override;
5857 void keyReleaseEvent(QKeyEvent * event) override;
5958
@@ -69,9 +68,6 @@ Q_SIGNALS:
6968 void released(const QString &shortcut);
7069 void activeChanged(bool active);
7170
72−private Q_SLOTS:
73− void setupFilterOnWindow(QQuickWindow* window);
74−
7571private:
7672 QVariant m_shortcut;
7773 bool m_active = true;
plugins/GlobalShortcut/globalshortcutregistry.cpp+23−15
@@ -21,11 +21,25 @@
2121
2222#include "globalshortcutregistry.h"
2323
24−static qulonglong s_windowId = 0;
24+namespace {
25+QWindow* windowForShortcut(GlobalShortcut *sc) {
26+ QObject* parent= sc;
27+ while(parent) {
28+ if (auto item = qobject_cast<QQuickItem*>(parent)) {
29+ auto window = item->window();
30+ if (window) return window;
31+ }
32+ parent = parent->parent();
33+ }
34+ return nullptr;
35+}
36+} // namespace
2537
2638GlobalShortcutRegistry::GlobalShortcutRegistry(QObject *parent)
2739 : QObject(parent)
2840{
41+ connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, &GlobalShortcutRegistry::setupFilterOnWindow);
42+ setupFilterOnWindow(qGuiApp->focusWindow());
2943}
3044
3145GlobalShortcutList GlobalShortcutRegistry::shortcuts() const
@@ -91,7 +105,10 @@ bool GlobalShortcutRegistry::eventFilter(QObject *obj, QEvent *event)
91105 const auto shortcuts = m_shortcuts.value(seq);
92106 Q_FOREACH(const auto &shortcut, shortcuts) {
93107 if (shortcut) {
94− qApp->sendEvent(shortcut, &eCopy);
108+ auto window = windowForShortcut(shortcut);
109+ if (!window || window == obj) { // accept shortcut if it's not attached to a window or it's window is active.
110+ qApp->sendEvent(shortcut, &eCopy);
111+ }
95112 }
96113 }
97114 }
@@ -102,24 +119,15 @@ bool GlobalShortcutRegistry::eventFilter(QObject *obj, QEvent *event)
102119 return QObject::eventFilter(obj, event);
103120}
104121
105−void GlobalShortcutRegistry::setupFilterOnWindow(qulonglong wid)
122+void GlobalShortcutRegistry::setupFilterOnWindow(QWindow* window)
106123{
107− if (wid == s_windowId) {
108− return;
109− }
110−
111124 if (m_filteredWindow) {
112125 m_filteredWindow->removeEventFilter(this);
113126 m_filteredWindow.clear();
114− s_windowId = 0;
115127 }
116128
117− Q_FOREACH(QWindow *window, qApp->allWindows()) {
118− if (window && window->winId() == wid) {
119− m_filteredWindow = window;
120− window->installEventFilter(this);
121− s_windowId = wid;
122− break;
123− }
129+ if (window) {
130+ m_filteredWindow = window;
131+ window->installEventFilter(this);
124132 }
125133}
plugins/GlobalShortcut/globalshortcutregistry.h+2−2
@@ -52,9 +52,9 @@ public:
5252 void addShortcut(const QVariant &seq, GlobalShortcut * sc);
5353
5454 /**
55− * Sets up key events filtering on window @p wid
55+ * Sets up key events filtering on window @p window
5656 */
57− void setupFilterOnWindow(qulonglong wid);
57+ void setupFilterOnWindow(QWindow* window);
5858
5959protected:
6060 bool eventFilter(QObject *obj, QEvent *event) override;
plugins/UInput/plugin.cpp+7−1
@@ -19,8 +19,14 @@
1919
2020#include <QtQml/qqml.h>
2121
22+
23+QObject* uinputSingleton(QQmlEngine*, QJSEngine*)
24+{
25+ return new UInput;
26+}
27+
2228void UInputPlugin::registerTypes(const char *uri)
2329{
2430 Q_ASSERT(uri == QLatin1String("UInput"));
25− qmlRegisterType<UInput>(uri, 0, 1, "UInput");
31+ qmlRegisterSingletonType<UInput>(uri, 0, 1, "UInput", uinputSingleton);
2632}
plugins/WindowManager/CMakeLists.txt+28−7
@@ -1,25 +1,46 @@
1+include_directories(
2+ SYSTEM
3+ ${QTMIRSERVER_INCLUDE_DIRS}
4+ ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
5+)
6+
7+include_directories(
8+ ${CMAKE_CURRENT_SOURCE_DIR}
9+ ${libunity8-private_SOURCE_DIR}
10+)
11+
112set(WINDOWMANAGER_SRC
213 AvailableDesktopArea.cpp
314 TopLevelWindowModel.cpp
415 Window.cpp
516 WindowManagerPlugin.cpp
617 WindowMargins.cpp
18+ Screen.cpp
19+ ScreenAttached.cpp
20+ Screens.cpp
21+ ScreensConfiguration.cpp
22+ ScreenWindow.cpp
23+ WindowManagerObjects.cpp
24+ Workspace.cpp
25+ WorkspaceManager.cpp
26+ WorkspaceModel.cpp
727 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
28+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
829 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/Mir.h
930 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceInterface.h
1031 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceListInterface.h
1132 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/SurfaceManagerInterface.h
12− )
33+)
1334
1435add_definitions(-DWINDOWMANAGERQML_LIBRARY)
1536
16−include_directories(
17− SYSTEM
18− ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
19−)
20−
2137add_library(windowmanager-qml SHARED ${WINDOWMANAGER_SRC})
2238
39+target_link_libraries(windowmanager-qml
40+ ${QTMIRSERVER_LDFLAGS}
41+ unity8-private
42+)
43+
2344qt5_use_modules(windowmanager-qml Qml Quick Gui)
2445
25−add_unity8_plugin(WindowManager 0.1 WindowManager TARGETS windowmanager-qml)
46+add_unity8_plugin(WindowManager 1.0 WindowManager TARGETS windowmanager-qml)
plugins/WindowManager/Screen.cppnew file+316
@@ -0,0 +1,316 @@
1+/*
2+ * Copyright (C) 2017 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "Screen.h"
18+#include "Screens.h"
19+#include "WorkspaceManager.h"
20+#include "Workspace.h"
21+
22+Screen::Screen(QObject *parent)
23+ : QObject(parent)
24+{
25+}
26+
27+void Screen::connectToScreen(qtmir::Screen *screen)
28+{
29+ m_wrapped = screen;
30+ connect(screen, &qtmir::Screen::usedChanged, this, &Screen::usedChanged);
31+ connect(screen, &qtmir::Screen::nameChanged, this, &Screen::nameChanged);
32+ connect(screen, &qtmir::Screen::outputTypeChanged, this, &Screen::outputTypeChanged);
33+ connect(screen, &qtmir::Screen::outputTypeChanged, this, &Screen::outputTypeNameChanged);
34+ connect(screen, &qtmir::Screen::scaleChanged, this, &Screen::scaleChanged);
35+ connect(screen, &qtmir::Screen::formFactorChanged, this, &Screen::formFactorChanged);
36+ connect(screen, &qtmir::Screen::physicalSizeChanged, this, &Screen::physicalSizeChanged);
37+ connect(screen, &qtmir::Screen::positionChanged, this, &Screen::positionChanged);
38+ connect(screen, &qtmir::Screen::activeChanged, this, &Screen::activeChanged);
39+ connect(screen, &qtmir::Screen::currentModeIndexChanged, this, &Screen::currentModeIndexChanged);
40+ connect(screen, &qtmir::Screen::availableModesChanged, this, &Screen::availableModesChanged);
41+}
42+
43+void Screen::connectToScreen(Screen *screen)
44+{
45+ connectToScreen(screen->wrapped());
46+ connect(screen, &Screen::currentWorkspaceChanged, this, &Screen::currentWorkspaceChanged);
47+}
48+
49+void Screen::setCurrentWorkspace2(Workspace *workspace)
50+{
51+ // Make sure we use the correct concrete class. Don't want to use a Proxy.
52+ workspace->setCurrentOn(this);
53+}
54+
55+bool Screen::used() const
56+{
57+ if (!m_wrapped) return false;
58+ return m_wrapped->used();
59+}
60+
61+QString Screen::name() const
62+{
63+ if (!m_wrapped) return QString();
64+ return m_wrapped->name();
65+}
66+
67+float Screen::scale() const
68+{
69+ if (!m_wrapped) return 1.0;
70+ return m_wrapped->scale();
71+}
72+
73+QSizeF Screen::physicalSize() const
74+{
75+ if (!m_wrapped) return QSizeF();
76+ return m_wrapped->physicalSize();
77+}
78+
79+qtmir::FormFactor Screen::formFactor() const
80+{
81+ if (!m_wrapped) return qtmir::FormFactorUnknown;
82+ return m_wrapped->formFactor();
83+}
84+
85+qtmir::OutputTypes Screen::outputType() const
86+{
87+ if (!m_wrapped) return qtmir::Unknown;
88+ return m_wrapped->outputType();
89+}
90+
91+MirPowerMode Screen::powerMode() const
92+{
93+ if (!m_wrapped) return mir_power_mode_on;
94+ return m_wrapped->powerMode();
95+}
96+
97+Qt::ScreenOrientation Screen::orientation() const
98+{
99+ if (!m_wrapped) return Qt::PrimaryOrientation;
100+ return m_wrapped->orientation();
101+}
102+
103+QPoint Screen::position() const
104+{
105+ if (!m_wrapped) return QPoint();
106+ return m_wrapped->position();
107+}
108+
109+QQmlListProperty<qtmir::ScreenMode> Screen::availableModes()
110+{
111+ if (!m_wrapped) return QQmlListProperty<qtmir::ScreenMode>();
112+ return m_wrapped->availableModes();
113+}
114+
115+uint Screen::currentModeIndex() const
116+{
117+ if (!m_wrapped) return -1;
118+ return m_wrapped->currentModeIndex();
119+}
120+
121+bool Screen::isActive() const
122+{
123+ if (!m_wrapped) return false;
124+ return m_wrapped->isActive();
125+}
126+
127+void Screen::activate()
128+{
129+ setActive(true);
130+}
131+
132+void Screen::setActive(bool active)
133+{
134+ if (!m_wrapped) return;
135+ m_wrapped->setActive(active);
136+}
137+
138+QScreen *Screen::qscreen() const
139+{
140+ if (!m_wrapped) return nullptr;
141+ return m_wrapped->qscreen();
142+}
143+
144+ScreenConfig *Screen::beginConfiguration() const
145+{
146+ if (!m_wrapped) return nullptr;
147+ return new ScreenConfig(m_wrapped->beginConfiguration());
148+}
149+
150+bool Screen::applyConfiguration(ScreenConfig *configuration)
151+{
152+ if (!m_wrapped) return false;
153+ return m_wrapped->applyConfiguration(configuration->m_config);
154+}
155+
156+QString Screen::outputTypeName() const
157+{
158+ switch (m_wrapped->outputType()) {
159+ case qtmir::Unknown:
160+ return tr("Unknown");
161+ case qtmir::VGA:
162+ return tr("VGA");
163+ case qtmir::DVII:
164+ case qtmir::DVID:
165+ case qtmir::DVIA:
166+ return tr("DVI");
167+ case qtmir::Composite:
168+ return tr("Composite");
169+ case qtmir::SVideo:
170+ return tr("S-Video");
171+ case qtmir::LVDS:
172+ case qtmir::NinePinDIN:
173+ case qtmir::EDP:
174+ return tr("Internal");
175+ case qtmir::Component:
176+ return tr("Component");
177+ case qtmir::DisplayPort:
178+ return tr("DisplayPort");
179+ case qtmir::HDMIA:
180+ case qtmir::HDMIB:
181+ return tr("HDMI");
182+ case qtmir::TV:
183+ return tr("TV");
184+ }
185+ return QString();
186+}
187+
188+bool Screen::isSameAs(Screen *screen) const
189+{
190+ if (!screen) return false;
191+ if (screen == this) return true;
192+ return wrapped() == screen->wrapped();
193+}
194+
195+void Screen::sync(Screen *proxy)
196+{
197+ if (!proxy) return;
198+ workspaces()->sync(proxy->workspaces());
199+}
200+
201+ConcreteScreen::ConcreteScreen(qtmir::Screen* wrapped)
202+ : m_workspaces(new WorkspaceModel)
203+{
204+ connectToScreen(wrapped);
205+
206+ // Connect the active workspace to activate the screen.
207+ connect(m_workspaces.data(), &WorkspaceModel::workspaceInserted, this, [this](int, Workspace* workspace) {
208+ connect(workspace, &Workspace::activeChanged, this, [this, workspace](bool active) {
209+ if (active) {
210+ setCurrentWorkspace(workspace);
211+ activate();
212+ }
213+ });
214+ if (workspace->isActive()) {
215+ activate();
216+ setCurrentWorkspace(workspace);
217+ }
218+ if (!m_currentWorspace) {
219+ setCurrentWorkspace(workspace);
220+ }
221+ });
222+ connect(m_workspaces.data(), &WorkspaceModel::workspaceRemoved, this, [this](Workspace* workspace) {
223+ disconnect(workspace, &Workspace::activeChanged, this, 0);
224+ if (workspace == m_currentWorspace) {
225+ resetCurrentWorkspace();
226+ }
227+ });
228+ connect(this, &ConcreteScreen::activeChanged, this, [this](bool active) {
229+ if (active && m_currentWorspace) {
230+ m_currentWorspace->activate();
231+ }
232+ });
233+}
234+
235+void ConcreteScreen::resetCurrentWorkspace()
236+{
237+ auto newCurrent = m_workspaces->rowCount() > 0 ? m_workspaces->get(0) : nullptr;
238+ if (m_currentWorspace != newCurrent) {
239+ m_currentWorspace = newCurrent;
240+ Q_EMIT currentWorkspaceChanged(newCurrent);
241+ }
242+}
243+
244+
245+WorkspaceModel *ConcreteScreen::workspaces() const
246+{
247+ return m_workspaces.data();
248+}
249+
250+Workspace *ConcreteScreen::currentWorkspace() const
251+{
252+ return m_currentWorspace.data();
253+}
254+
255+void ConcreteScreen::setCurrentWorkspace(Workspace *workspace)
256+{
257+ if (m_currentWorspace != workspace) {
258+ m_currentWorspace = workspace;
259+ Q_EMIT currentWorkspaceChanged(workspace);
260+ }
261+}
262+
263+ProxyScreen::ProxyScreen(Screen *const screen, ProxyScreens* screens)
264+ : m_workspaces(new ProxyWorkspaceModel(screen->workspaces(), this))
265+ , m_original(screen)
266+ , m_screens(screens)
267+{
268+ connectToScreen(screen);
269+
270+ auto updateCurrentWorkspaceFn = [this](Workspace* realWorkspace) {
271+ Q_FOREACH(Workspace* workspace, m_workspaces->list()) {
272+ auto p = qobject_cast<ProxyWorkspace*>(workspace);
273+ if (p && p->proxyObject() == realWorkspace) {
274+ if (m_currentWorspace != p) {
275+ m_currentWorspace = p;
276+ Q_EMIT currentWorkspaceChanged(p);
277+ }
278+ }
279+ }
280+ };
281+ connect(screen, &Screen::currentWorkspaceChanged, this, updateCurrentWorkspaceFn);
282+ updateCurrentWorkspaceFn(screen->currentWorkspace());
283+}
284+
285+WorkspaceModel *ProxyScreen::workspaces() const
286+{
287+ return m_workspaces.data();
288+}
289+
290+Workspace *ProxyScreen::currentWorkspace() const
291+{
292+ return m_currentWorspace.data();
293+}
294+
295+void ProxyScreen::setCurrentWorkspace(Workspace *workspace)
296+{
297+ auto p = qobject_cast<ProxyWorkspace*>(workspace);
298+ if (p) {
299+ m_original->setCurrentWorkspace(p->proxyObject());
300+ }
301+}
302+
303+bool ProxyScreen::isSyncing() const
304+{
305+ return m_screens->isSyncing();
306+}
307+
308+ScreenConfig::ScreenConfig(qtmir::ScreenConfiguration *config)
309+ : m_config(config)
310+{
311+}
312+
313+ScreenConfig::~ScreenConfig()
314+{
315+ delete m_config;
316+}
plugins/WindowManager/Screen.hnew file+153
@@ -0,0 +1,153 @@
1+#ifndef SCREEN_H
2+#define SCREEN_H
3+
4+#include <qtmir/screen.h>
5+#include <QScopedPointer>
6+#include <QPointer>
7+
8+#include "WorkspaceModel.h"
9+
10+class ProxyScreen;
11+class ProxyScreens;
12+class ScreenConfig;
13+
14+class Screen: public QObject
15+{
16+ Q_OBJECT
17+
18+ Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged)
19+ Q_PROPERTY(bool used READ used NOTIFY usedChanged)
20+ Q_PROPERTY(QString name READ name NOTIFY nameChanged)
21+ Q_PROPERTY(qtmir::OutputTypes outputType READ outputType NOTIFY outputTypeChanged)
22+ Q_PROPERTY(float scale READ scale NOTIFY scaleChanged)
23+ Q_PROPERTY(qtmir::FormFactor formFactor READ formFactor NOTIFY formFactorChanged)
24+ Q_PROPERTY(MirPowerMode powerMode READ powerMode NOTIFY powerModeChanged)
25+ Q_PROPERTY(Qt::ScreenOrientation orientation READ orientation NOTIFY orientationChanged)
26+ Q_PROPERTY(QPoint position READ position NOTIFY positionChanged)
27+ Q_PROPERTY(uint currentModeIndex READ currentModeIndex NOTIFY currentModeIndexChanged)
28+ Q_PROPERTY(QQmlListProperty<qtmir::ScreenMode> availableModes READ availableModes NOTIFY availableModesChanged)
29+ Q_PROPERTY(QSizeF physicalSize READ physicalSize NOTIFY physicalSizeChanged)
30+ Q_PROPERTY(QString outputTypeName READ outputTypeName NOTIFY outputTypeChanged)
31+ Q_PROPERTY(WorkspaceModel* workspaces READ workspaces CONSTANT)
32+ Q_PROPERTY(Workspace* currentWorkspace READ currentWorkspace WRITE setCurrentWorkspace2 NOTIFY currentWorkspaceChanged)
33+public:
34+ bool used() const;
35+ QString name() const;
36+ float scale() const;
37+ QSizeF physicalSize() const;
38+ qtmir::FormFactor formFactor() const;
39+ qtmir::OutputTypes outputType() const;
40+ MirPowerMode powerMode() const;
41+ Qt::ScreenOrientation orientation() const;
42+ QPoint position() const;
43+ QQmlListProperty<qtmir::ScreenMode> availableModes();
44+ uint currentModeIndex() const;
45+ bool isActive() const;
46+ void setActive(bool active);
47+ QScreen* qscreen() const;
48+ QString outputTypeName() const;
49+
50+ Q_INVOKABLE bool isSameAs(Screen*) const;
51+
52+ Q_INVOKABLE ScreenConfig *beginConfiguration() const;
53+ Q_INVOKABLE bool applyConfiguration(ScreenConfig *configuration);
54+
55+ virtual WorkspaceModel* workspaces() const = 0;
56+ virtual Workspace *currentWorkspace() const = 0;
57+ virtual void setCurrentWorkspace(Workspace* workspace) = 0;
58+
59+ void sync(Screen* proxy);
60+
61+ qtmir::Screen* wrapped() const { return m_wrapped; }
62+
63+public Q_SLOTS:
64+ void activate();
65+
66+Q_SIGNALS:
67+ void usedChanged();
68+ void nameChanged();
69+ void outputTypeChanged();
70+ void outputTypeNameChanged();
71+ void scaleChanged();
72+ void formFactorChanged();
73+ void powerModeChanged();
74+ void orientationChanged();
75+ void positionChanged();
76+ void currentModeIndexChanged();
77+ void physicalSizeChanged();
78+ void availableModesChanged();
79+ void activeChanged(bool active);
80+ void currentWorkspaceChanged(Workspace*);
81+
82+protected:
83+ Screen(QObject* parent = 0);
84+
85+ void connectToScreen(qtmir::Screen* screen);
86+ void connectToScreen(Screen* screen);
87+
88+private:
89+ void setCurrentWorkspace2(Workspace* workspace);
90+
91+protected:
92+ QPointer<qtmir::Screen> m_wrapped;
93+};
94+
95+
96+class ConcreteScreen : public Screen
97+{
98+ Q_OBJECT
99+public:
100+ explicit ConcreteScreen(qtmir::Screen*const wrapped);
101+
102+ // From qtmir::Screen
103+ WorkspaceModel* workspaces() const override;
104+ Workspace *currentWorkspace() const override;
105+ void setCurrentWorkspace(Workspace* workspace) override;
106+
107+protected:
108+ void resetCurrentWorkspace();
109+
110+ const QScopedPointer<WorkspaceModel> m_workspaces;
111+ QPointer<Workspace> m_currentWorspace;
112+};
113+
114+class ProxyScreen : public Screen
115+{
116+ Q_OBJECT
117+public:
118+ explicit ProxyScreen(Screen*const screen, ProxyScreens* screens);
119+
120+ // From qtmir::Screen
121+ WorkspaceModel* workspaces() const override;
122+ Workspace *currentWorkspace() const override;
123+ void setCurrentWorkspace(Workspace* workspace) override;
124+
125+ Screen* proxyObject() const { return m_original.data(); }
126+
127+ bool isSyncing() const;
128+
129+private:
130+ const QScopedPointer<WorkspaceModel> m_workspaces;
131+ const QPointer<Screen> m_original;
132+ const ProxyScreens* m_screens;
133+ QPointer<Workspace> m_currentWorspace;
134+};
135+
136+class ScreenConfig: public QObject
137+{
138+ Q_OBJECT
139+ Q_PRIVATE_PROPERTY(m_config, bool valid MEMBER used CONSTANT)
140+ Q_PRIVATE_PROPERTY(m_config, bool used MEMBER used)
141+ Q_PRIVATE_PROPERTY(m_config, float scale MEMBER scale)
142+ Q_PRIVATE_PROPERTY(m_config, qtmir::FormFactor formFactor MEMBER formFactor)
143+ Q_PRIVATE_PROPERTY(m_config, uint currentModeIndex MEMBER currentModeIndex)
144+ Q_PRIVATE_PROPERTY(m_config, QPoint position MEMBER topLeft)
145+
146+public:
147+ ScreenConfig(qtmir::ScreenConfiguration*);
148+ ~ScreenConfig();
149+
150+ qtmir::ScreenConfiguration* m_config;
151+};
152+
153+#endif // SCREEN_H
plugins/WindowManager/ScreenAttached.cppnew file+164
@@ -0,0 +1,164 @@
1+/*
2+ * Copyright (C) 2017 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "ScreenAttached.h"
18+#include "ScreenWindow.h"
19+#include "Screens.h"
20+
21+#include <QQuickItem>
22+#include <QScreen>
23+
24+namespace
25+{
26+QQuickItem* itemForOwner(QObject* obj) {
27+ QObject* parent = obj;
28+ while(parent) {
29+ auto item = qobject_cast<QQuickItem*>(parent);
30+ if (item) return item;
31+ parent = parent->parent();
32+ }
33+ return nullptr;
34+}
35+} // namesapce
36+
37+ScreenAttached::ScreenAttached(QObject *owner)
38+ : Screen(owner)
39+ , m_window(nullptr)
40+{
41+ if (auto item = itemForOwner(owner)) {
42+ connect(item, &QQuickItem::windowChanged, this, &ScreenAttached::windowChanged);
43+ windowChanged(item->window());
44+ } else if (auto window = qobject_cast<QQuickWindow*>(owner)) {
45+ windowChanged(window);
46+ }
47+}
48+
49+WorkspaceModel *ScreenAttached::workspaces() const
50+{
51+ if (!m_screen) return nullptr;
52+ return m_screen->workspaces();
53+}
54+
55+Workspace *ScreenAttached::currentWorkspace() const
56+{
57+ if (!m_screen) return nullptr;
58+ return m_screen->currentWorkspace();
59+}
60+
61+void ScreenAttached::setCurrentWorkspace(Workspace *workspace)
62+{
63+ if (!m_screen) return;
64+ return m_screen->setCurrentWorkspace(workspace);
65+}
66+
67+void ScreenAttached::windowChanged(QQuickWindow *window)
68+{
69+ if (m_window) {
70+ disconnect(m_window, &QWindow::screenChanged, this, &ScreenAttached::screenChanged);
71+ }
72+
73+ m_window = window;
74+ auto screenWindow = qobject_cast<ScreenWindow*>(window);
75+
76+ if (screenWindow) {
77+ screenChanged2(screenWindow->screenWrapper());
78+ connect(screenWindow, &ScreenWindow::screenWrapperChanged, this, &ScreenAttached::screenChanged2);
79+ } else {
80+ screenChanged(window ? window->screen() : NULL);
81+ if (window) {
82+ connect(window, &QWindow::screenChanged, this, &ScreenAttached::screenChanged);
83+ }
84+ }
85+}
86+
87+void ScreenAttached::screenChanged(QScreen *qscreen)
88+{
89+ // Find a screen that matches.
90+ // Should only get here in mocks if we don't have a ScreenWindow
91+ Screen* screen{nullptr};
92+ Q_FOREACH(auto s, ConcreteScreens::self()->list()) {
93+ if (s->qscreen() == qscreen) {
94+ screen = s;
95+ }
96+ }
97+ screenChanged2(screen);
98+}
99+
100+void ScreenAttached::screenChanged2(Screen* screen)
101+{
102+ if (screen == m_screen) return;
103+
104+ Screen* oldScreen = m_screen;
105+ m_screen = screen;
106+
107+ if (oldScreen)
108+ oldScreen->disconnect(this);
109+
110+ if (!screen)
111+ return; //Don't bother emitting signals, because the new values are garbage anyways
112+
113+ if (!oldScreen || screen->isActive() != oldScreen->isActive())
114+ Q_EMIT activeChanged(screen->isActive());
115+ if (!oldScreen || screen->used() != oldScreen->used())
116+ Q_EMIT usedChanged();
117+ if (!oldScreen || screen->name() != oldScreen->name())
118+ Q_EMIT nameChanged();
119+ if (!oldScreen || screen->outputType() != oldScreen->outputType())
120+ Q_EMIT outputTypeChanged();
121+ if (!oldScreen || screen->scale() != oldScreen->scale())
122+ Q_EMIT scaleChanged();
123+ if (!oldScreen || screen->formFactor() != oldScreen->formFactor())
124+ Q_EMIT formFactorChanged();
125+ if (!oldScreen || screen->powerMode() != oldScreen->powerMode())
126+ Q_EMIT powerModeChanged();
127+ if (!oldScreen || screen->orientation() != oldScreen->orientation())
128+ Q_EMIT orientationChanged();
129+ if (!oldScreen || screen->position() != oldScreen->position())
130+ Q_EMIT positionChanged();
131+ if (!oldScreen || screen->currentModeIndex() != oldScreen->currentModeIndex())
132+ Q_EMIT currentModeIndexChanged();
133+ if (!oldScreen || screen->physicalSize() != oldScreen->physicalSize())
134+ Q_EMIT physicalSizeChanged();
135+ if (!oldScreen || screen->currentWorkspace() != oldScreen->currentWorkspace())
136+ Q_EMIT currentWorkspaceChanged(currentWorkspace());
137+
138+ if (oldScreen) {
139+ QVector<qtmir::ScreenMode*> oldModes;
140+ auto oldModesQmlList = oldScreen->availableModes();
141+ for (int i = 0; i < oldModesQmlList.count(&oldModesQmlList); i++) {
142+ oldModes << oldModesQmlList.at(&oldModesQmlList, i);
143+ }
144+
145+ QVector<qtmir::ScreenMode*> newModes;
146+ auto newModesQmlList = screen->availableModes();
147+ for (int i = 0; i < newModesQmlList.count(&newModesQmlList); i++) {
148+ newModes << newModesQmlList.at(&newModesQmlList, i);
149+ }
150+
151+ if (newModes != newModes) {
152+ Q_EMIT availableModesChanged();
153+ }
154+ } else {
155+ Q_EMIT availableModesChanged();
156+ }
157+
158+ connectToScreen(screen);
159+}
160+
161+ScreenAttached *WMScreen::qmlAttachedProperties(QObject *owner)
162+{
163+ return new ScreenAttached(owner);
164+}
plugins/WindowManager/ScreenAttached.hnew file+56
@@ -0,0 +1,56 @@
1+/*
2+ * Copyright (C) 2017 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef SCREENATTACHED_H
18+#define SCREENATTACHED_H
19+
20+#include "Screen.h"
21+
22+#include <QQmlEngine>
23+#include <QtQml>
24+
25+class QQuickWindow;
26+
27+class ScreenAttached : public Screen
28+{
29+ Q_OBJECT
30+public:
31+ ScreenAttached(QObject* owner);
32+
33+ WorkspaceModel* workspaces() const override;
34+ Workspace *currentWorkspace() const override;
35+ void setCurrentWorkspace(Workspace* workspace) override;
36+
37+private Q_SLOTS:
38+ void windowChanged(QQuickWindow*);
39+ void screenChanged(QScreen*);
40+ void screenChanged2(Screen* screen);
41+
42+private:
43+ QPointer<Screen> m_screen;
44+ QQuickWindow* m_window;
45+};
46+
47+class WMScreen : public QObject
48+{
49+ Q_OBJECT
50+public:
51+ static ScreenAttached *qmlAttachedProperties(QObject *owner);
52+};
53+
54+QML_DECLARE_TYPEINFO(WMScreen, QML_HAS_ATTACHED_PROPERTIES)
55+
56+#endif // SCREENATTACHED_H
plugins/WindowManager/ScreenWindow.cppnew file+45
@@ -0,0 +1,45 @@
1+/*
2+ * Copyright (C) 2016-2017 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "ScreenWindow.h"
18+#include "Screen.h"
19+
20+// Qt
21+#include <QGuiApplication>
22+#include <QDebug>
23+
24+ScreenWindow::ScreenWindow(QQuickWindow *parent)
25+ : QQuickWindow(parent)
26+{
27+}
28+
29+ScreenWindow::~ScreenWindow()
30+{
31+}
32+
33+ConcreteScreen *ScreenWindow::screenWrapper() const
34+{
35+ return m_screen.data();
36+}
37+
38+void ScreenWindow::setScreenWrapper(ConcreteScreen *screen)
39+{
40+ if (m_screen != screen) {
41+ m_screen = screen;
42+ Q_EMIT screenWrapperChanged(screen);
43+ }
44+ QQuickWindow::setScreen(screen->qscreen());
45+}
plugins/WindowManager/ScreenWindow.hnew file+49
@@ -0,0 +1,49 @@
1+/*
2+ * Copyright (C) 2016-2017 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UNITY_SCREENWINDOW_H
18+#define UNITY_SCREENWINDOW_H
19+
20+#include <QQuickWindow>
21+#include <QPointer>
22+
23+#include "Screen.h"
24+
25+class ScreenAdapter;
26+
27+/*
28+ * ScreenWindow - wrapper of QQuickWindow to enable QML to specify destination screen.
29+**/
30+class ScreenWindow : public QQuickWindow
31+{
32+ Q_OBJECT
33+ Q_PROPERTY(ConcreteScreen *screen READ screenWrapper WRITE setScreenWrapper NOTIFY screenWrapperChanged)
34+ Q_PROPERTY(int winId READ winId CONSTANT)
35+public:
36+ explicit ScreenWindow(QQuickWindow *parent = 0);
37+ ~ScreenWindow();
38+
39+ ConcreteScreen *screenWrapper() const;
40+ void setScreenWrapper(ConcreteScreen *screen);
41+
42+Q_SIGNALS:
43+ void screenWrapperChanged(ConcreteScreen* screen);
44+
45+private:
46+ QPointer<ConcreteScreen> m_screen;
47+};
48+
49+#endif // UNITY_SCREENWINDOW_H
plugins/WindowManager/Screens.cppnew file+245
@@ -0,0 +1,245 @@
1+/*
2+ * Copyright (C) 2016-2017 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "Screens.h"
18+#include "ScreensConfiguration.h"
19+#include "Screen.h"
20+#include "WorkspaceManager.h"
21+
22+// qtmirserver
23+#include <qtmir/screens.h>
24+#include <QGuiApplication>
25+#include <QQmlEngine>
26+
27+// Qt
28+#include <QScreen>
29+#include <QWindow>
30+
31+ConcreteScreens* ConcreteScreens::m_self{nullptr};
32+
33+Screens::Screens(const QSharedPointer<qtmir::Screens>& model)
34+ : m_wrapped(model)
35+{
36+}
37+
38+Screens::~Screens()
39+{
40+ qDeleteAll(m_screens);
41+ m_screens.clear();
42+}
43+
44+QHash<int, QByteArray> Screens::roleNames() const
45+{
46+ QHash<int, QByteArray> roles;
47+ roles[ScreenRole] = "screen";
48+ return roles;
49+}
50+
51+QVariant Screens::data(const QModelIndex &index, int role) const
52+{
53+ if (!index.isValid() || index.row() >= m_screens.size()) {
54+ return QVariant();
55+ }
56+
57+ switch(role) {
58+ case ScreenRole:
59+ return QVariant::fromValue(m_screens.at(index.row()));
60+ } // switch
61+
62+ return QVariant();
63+}
64+
65+int Screens::rowCount(const QModelIndex &) const
66+{
67+ return count();
68+}
69+
70+int Screens::indexOf(Screen *screen) const
71+{
72+ return m_screens.indexOf(screen);
73+}
74+
75+Screen *Screens::get(int index) const
76+{
77+ return m_screens.at(index);
78+}
79+
80+int Screens::count() const
81+{
82+ return m_screens.size();
83+}
84+
85+QVariant Screens::activeScreen() const
86+{
87+ for (int i = 0; i < m_screens.count(); i++) {
88+ if (m_screens[i]->isActive()) return i;
89+ }
90+ return QVariant();
91+}
92+
93+void Screens::activateScreen(const QVariant& vindex)
94+{
95+ bool ok = false;
96+ int index = vindex.toInt(&ok);
97+ if (!ok || index < 0 || m_screens.count() <= index) return;
98+
99+ auto screen = m_screens.at(index);
100+ screen->setActive(true);
101+}
102+
103+
104+ConcreteScreens::ConcreteScreens(const QSharedPointer<qtmir::Screens> &model, ScreensConfiguration* config)
105+ : Screens(model)
106+ , m_config(config)
107+{
108+ m_self = this;
109+ connect(m_wrapped.data(), &qtmir::Screens::screenAdded, this, &ConcreteScreens::onScreenAdded);
110+ connect(m_wrapped.data(), &qtmir::Screens::screenRemoved, this, &ConcreteScreens::onScreenRemoved);
111+ connect(m_wrapped.data(), &qtmir::Screens::activeScreenChanged, this, &ConcreteScreens::activeScreenChanged);
112+
113+ Q_FOREACH(qtmir::Screen* screen, m_wrapped->screens()) {
114+ auto screenWrapper(new ConcreteScreen(screen));
115+ m_config->load(screenWrapper);
116+
117+ QQmlEngine::setObjectOwnership(screenWrapper, QQmlEngine::CppOwnership);
118+ m_screens.push_back(screenWrapper);
119+ }
120+}
121+
122+ConcreteScreens::~ConcreteScreens()
123+{
124+ Q_FOREACH(Screen* screen, m_screens) {
125+ m_config->save(screen);
126+ }
127+ delete m_config;
128+}
129+
130+ConcreteScreens *ConcreteScreens::self()
131+{
132+ return ConcreteScreens::m_self;
133+}
134+
135+ProxyScreens *ConcreteScreens::createProxy()
136+{
137+ return new ProxyScreens(this);
138+}
139+
140+void ConcreteScreens::sync(ProxyScreens *proxy)
141+{
142+ if (!proxy) return;
143+ proxy->setSyncing(true);
144+
145+ const auto& proxyList = proxy->list();
146+ for (int i = 0; i < m_screens.count() && i < proxyList.count(); ++i) {
147+ m_screens[i]->sync(proxyList[i]);
148+ }
149+
150+ proxy->setSyncing(false);
151+}
152+
153+void ConcreteScreens::onScreenAdded(qtmir::Screen *added)
154+{
155+ Q_FOREACH(auto screenWrapper, m_screens) {
156+ if (screenWrapper->wrapped() == added) return;
157+ }
158+
159+ beginInsertRows(QModelIndex(), count(), count());
160+ auto screenWrapper(new ConcreteScreen(added));
161+ m_config->load(screenWrapper);
162+
163+ QQmlEngine::setObjectOwnership(screenWrapper, QQmlEngine::CppOwnership);
164+ m_screens.push_back(screenWrapper);
165+ endInsertRows();
166+ Q_EMIT screenAdded(screenWrapper);
167+ Q_EMIT countChanged();
168+}
169+
170+void ConcreteScreens::onScreenRemoved(qtmir::Screen *removed)
171+{
172+ int index = 0;
173+ QMutableVectorIterator<Screen*> iter(m_screens);
174+ while(iter.hasNext()) {
175+ auto screenWrapper = iter.next();
176+ if (screenWrapper->wrapped() == removed) {
177+ m_config->save(screenWrapper);
178+
179+ beginRemoveRows(QModelIndex(), index, index);
180+ iter.remove();
181+ endRemoveRows();
182+
183+ Q_EMIT screenRemoved(screenWrapper);
184+ Q_EMIT countChanged();
185+
186+ screenWrapper->deleteLater();
187+ break;
188+ }
189+ index++;
190+ }
191+}
192+
193+
194+ProxyScreens::ProxyScreens(Screens * const screens)
195+ : Screens(screens->m_wrapped)
196+ , m_original(screens)
197+ , m_syncing(false)
198+{
199+ connect(screens, &Screens::screenAdded, this, [this](Screen *added) {
200+ Q_FOREACH(auto screen, m_screens) {
201+ auto proxy = static_cast<ProxyScreen*>(screen);
202+ if (proxy->proxyObject() == added) return;
203+ }
204+
205+ beginInsertRows(QModelIndex(), count(), count());
206+ auto screenWrapper(new ProxyScreen(added, this));
207+ QQmlEngine::setObjectOwnership(screenWrapper, QQmlEngine::CppOwnership);
208+ m_screens.push_back(screenWrapper);
209+ endInsertRows();
210+ Q_EMIT screenAdded(screenWrapper);
211+ Q_EMIT countChanged();
212+ });
213+
214+ connect(screens, &Screens::screenRemoved, this, [this](Screen *removed) {
215+ int index = 0;
216+ QMutableVectorIterator<Screen*> iter(m_screens);
217+ while(iter.hasNext()) {
218+ auto proxy = static_cast<ProxyScreen*>(iter.next());
219+ if (proxy->proxyObject() == removed) {
220+
221+ beginRemoveRows(QModelIndex(), index, index);
222+ iter.remove();
223+ endRemoveRows();
224+
225+ Q_EMIT screenRemoved(proxy);
226+ Q_EMIT countChanged();
227+
228+ delete proxy;
229+ break;
230+ }
231+ index++;
232+ }
233+ });
234+
235+ Q_FOREACH(Screen* screen, screens->list()) {
236+ auto screenWrapper(new ProxyScreen(screen, this));
237+ QQmlEngine::setObjectOwnership(screenWrapper, QQmlEngine::CppOwnership);
238+ m_screens.push_back(screenWrapper);
239+ }
240+}
241+
242+void ProxyScreens::setSyncing(bool syncing)
243+{
244+ m_syncing = syncing;
245+}
plugins/WindowManager/Screens.hnew file+114
@@ -0,0 +1,114 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UNITY_SCREENS_H
18+#define UNITY_SCREENS_H
19+
20+#include <QAbstractListModel>
21+#include <QSharedPointer>
22+#include <QPointer>
23+
24+namespace qtmir
25+{
26+class Screen;
27+class Screens;
28+}
29+
30+class Screen;
31+class ProxyScreens;
32+class ScreensConfiguration;
33+
34+class Screens : public QAbstractListModel
35+{
36+ Q_OBJECT
37+ Q_PROPERTY(int count READ count NOTIFY countChanged)
38+ Q_PROPERTY(QVariant activeScreen READ activeScreen WRITE activateScreen NOTIFY activeScreenChanged)
39+
40+public:
41+ enum ItemRoles {
42+ ScreenRole = Qt::UserRole + 1
43+ };
44+
45+ virtual ~Screens();
46+
47+ /* QAbstractItemModel */
48+ QHash<int, QByteArray> roleNames() const override;
49+ QVariant data(const QModelIndex &index, int role = ScreenRole) const override;
50+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
51+
52+ Q_INVOKABLE int indexOf(Screen*) const;
53+ Q_INVOKABLE Screen* get(int index) const;
54+
55+ int count() const;
56+ QVariant activeScreen() const;
57+
58+ const QVector<Screen*>& list() const { return m_screens; }
59+
60+public Q_SLOTS:
61+ void activateScreen(const QVariant& index);
62+
63+Q_SIGNALS:
64+ void countChanged();
65+ void activeScreenChanged();
66+
67+ void screenAdded(Screen* screen);
68+ void screenRemoved(Screen* screen);
69+
70+protected:
71+ Screens(const QSharedPointer<qtmir::Screens>& model);
72+
73+ QVector<Screen*> m_screens;
74+ const QSharedPointer<qtmir::Screens> m_wrapped;
75+
76+ friend class ProxyScreens;
77+};
78+
79+class ConcreteScreens : public Screens
80+{
81+ Q_OBJECT
82+public:
83+ explicit ConcreteScreens(const QSharedPointer<qtmir::Screens>& model, ScreensConfiguration* config);
84+ ~ConcreteScreens();
85+
86+ Q_INVOKABLE ProxyScreens *createProxy();
87+ Q_INVOKABLE void sync(ProxyScreens *proxy);
88+
89+ static ConcreteScreens *self();
90+
91+protected Q_SLOTS:
92+ void onScreenAdded(qtmir::Screen *screen);
93+ void onScreenRemoved(qtmir::Screen *screen);
94+
95+private:
96+ ScreensConfiguration* m_config;
97+
98+ static ConcreteScreens* m_self;
99+};
100+
101+class ProxyScreens : public Screens
102+{
103+public:
104+ explicit ProxyScreens(Screens*const screens);
105+
106+ void setSyncing(bool syncing);
107+ bool isSyncing() const { return m_syncing; }
108+
109+private:
110+ const QPointer<Screens> m_original;
111+ bool m_syncing;
112+};
113+
114+#endif // SCREENS_H
plugins/WindowManager/ScreensConfiguration.cppnew file+94
@@ -0,0 +1,94 @@
1+/*
2+ * Copyright (C) 2017 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+#include "ScreensConfiguration.h"
18+#include "Screen.h"
19+#include "Workspace.h"
20+#include "WorkspaceManager.h"
21+
22+#include <QJsonObject>
23+#include <QJsonDocument>
24+#include <QJsonArray>
25+#include <QFile>
26+#include <QStandardPaths>
27+
28+namespace
29+{
30+QJsonArray jsonScreens;
31+}
32+
33+ScreensConfiguration::ScreensConfiguration()
34+{
35+ const QString dbPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QStringLiteral("/unity8/");
36+ QFile f(dbPath + "workspaces");
37+
38+ if (f.open(QIODevice::ReadOnly)) {
39+ QByteArray saveData = f.readAll();
40+ QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
41+ QJsonObject json(loadDoc.object());
42+ jsonScreens = json["screens"].toArray();
43+ }
44+}
45+
46+ScreensConfiguration::~ScreensConfiguration()
47+{
48+ const QString dbPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QStringLiteral("/unity8/");
49+ QFile f(dbPath + "workspaces");
50+ if (f.open(QIODevice::WriteOnly)) {
51+ QJsonObject json;
52+ json["screens"] = jsonScreens;
53+ QJsonDocument saveDoc(json);
54+ f.write(saveDoc.toJson());
55+ }
56+}
57+
58+void ScreensConfiguration::load(Screen *screen)
59+{
60+ int workspaces = 2;
61+ for (auto iter = jsonScreens.begin(); iter != jsonScreens.end(); ++iter) {
62+ QJsonObject jsonScreen = (*iter).toObject();
63+ if (jsonScreen["name"] == screen->name()) {
64+ QJsonValue jsonWorkspaces = jsonScreen["workspaces"];
65+ workspaces = qMax(jsonWorkspaces.toInt(workspaces), 1);
66+ break;
67+ }
68+ }
69+
70+ for (int i = 0; i < workspaces; i++) {
71+ WorkspaceManager::instance()->createWorkspace()->assign(screen->workspaces());
72+ }
73+}
74+
75+void ScreensConfiguration::save(Screen *screen)
76+{
77+ QJsonObject newJsonScreen;
78+ newJsonScreen["name"] = screen->name();
79+ newJsonScreen["workspaces"] = qMax(screen->workspaces()->rowCount(), 1);
80+
81+ auto iter = jsonScreens.begin();
82+ for (; iter != jsonScreens.end(); ++iter) {
83+ QJsonObject jsonScreen = (*iter).toObject();
84+ if (jsonScreen["name"] == screen->name()) {
85+ break;
86+ }
87+ }
88+
89+ if (iter == jsonScreens.end()) {
90+ jsonScreens.push_back(newJsonScreen);
91+ } else {
92+ *iter = newJsonScreen;
93+ }
94+}
src/ShellView.h → plugins/WindowManager/ScreensConfiguration.hrenamed+10−12
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2017 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -14,21 +14,19 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17−#ifndef UNITY_SHELL_VIEW_H
18−#define UNITY_SHELL_VIEW_H
17+#ifndef SCREENSCONFIGURATION_H
18+#define SCREENSCONFIGURATION_H
1919
20−#include <QQuickView>
20+class Screen;
2121
22−class ShellView : public QQuickView
22+class ScreensConfiguration
2323{
24− Q_OBJECT
25−
2624public:
27− ShellView(QQmlEngine *engine, QObject *qmlArgs);
25+ explicit ScreensConfiguration();
26+ ~ScreensConfiguration();
2827
29−private Q_SLOTS:
30− void onWidthChanged(int);
31− void onHeightChanged(int);
28+ void load(Screen* screen);
29+ void save(Screen* screen);
3230};
3331
34−#endif // UNITY_SHELL_VIEW_H
32+#endif // SCREENSCONFIGURATION_H
plugins/WindowManager/TopLevelWindowModel.cpp+207−75
@@ -15,6 +15,7 @@
1515 */
1616
1717#include "TopLevelWindowModel.h"
18+#include "WindowManagerObjects.h"
1819
1920// unity-api
2021#include <unity/shell/application/ApplicationInfoInterface.h>
@@ -24,11 +25,11 @@
2425#include <unity/shell/application/SurfaceManagerInterface.h>
2526
2627// Qt
27−#include <QGuiApplication>
2828#include <QDebug>
2929
3030// local
3131#include "Window.h"
32+#include "Workspace.h"
3233
3334Q_LOGGING_CATEGORY(TOPLEVELWINDOWMODEL, "toplevelwindowmodel", QtInfoMsg)
3435
@@ -37,8 +38,20 @@ Q_LOGGING_CATEGORY(TOPLEVELWINDOWMODEL, "toplevelwindowmodel", QtInfoMsg)
3738
3839namespace unityapi = unity::shell::application;
3940
40−TopLevelWindowModel::TopLevelWindowModel()
41− : m_nullWindow(createWindow(nullptr))
41+TopLevelWindowModel::TopLevelWindowModel(Workspace* workspace)
42+ : m_nullWindow(createWindow(nullptr)),
43+ m_workspace(workspace)
44+{
45+ connect(WindowManagerObjects::instance(), &WindowManagerObjects::applicationManagerChanged,
46+ this, &TopLevelWindowModel::setApplicationManager);
47+ connect(WindowManagerObjects::instance(), &WindowManagerObjects::surfaceManagerChanged,
48+ this, &TopLevelWindowModel::setSurfaceManager);
49+
50+ setApplicationManager(WindowManagerObjects::instance()->applicationManager());
51+ setSurfaceManager(WindowManagerObjects::instance()->surfaceManager());
52+}
53+
54+TopLevelWindowModel::~TopLevelWindowModel()
4255{
4356}
4457
@@ -56,7 +69,6 @@ void TopLevelWindowModel::setApplicationManager(unityapi::ApplicationManagerInte
5669 beginResetModel();
5770
5871 if (m_applicationManager) {
59− m_windowModel.clear();
6072 disconnect(m_applicationManager, 0, this, 0);
6173 }
6274
@@ -65,26 +77,26 @@ void TopLevelWindowModel::setApplicationManager(unityapi::ApplicationManagerInte
6577 if (m_applicationManager) {
6678 connect(m_applicationManager, &QAbstractItemModel::rowsInserted,
6779 this, [this](const QModelIndex &/*parent*/, int first, int last) {
68− for (int i = first; i <= last; ++i) {
69− auto application = m_applicationManager->get(i);
70− addApplication(application);
71− }
72− });
80+ if (!m_workspace || !m_workspace->isActive())
81+ return;
82+
83+ for (int i = first; i <= last; ++i) {
84+ auto application = m_applicationManager->get(i);
85+ addApplication(application);
86+ }
87+ });
7388
7489 connect(m_applicationManager, &QAbstractItemModel::rowsAboutToBeRemoved,
7590 this, [this](const QModelIndex &/*parent*/, int first, int last) {
76− for (int i = first; i <= last; ++i) {
77− auto application = m_applicationManager->get(i);
78− removeApplication(application);
79− }
80− });
81−
82− for (int i = 0; i < m_applicationManager->rowCount(); ++i) {
83− auto application = m_applicationManager->get(i);
84− addApplication(application);
85− }
91+ for (int i = first; i <= last; ++i) {
92+ auto application = m_applicationManager->get(i);
93+ removeApplication(application);
94+ }
95+ });
8696 }
8797
98+ refreshWindows();
99+
88100 endResetModel();
89101 m_modelState = IdleState;
90102}
@@ -97,6 +109,11 @@ void TopLevelWindowModel::setSurfaceManager(unityapi::SurfaceManagerInterface *s
97109
98110 DEBUG_MSG << "(" << surfaceManager << ")";
99111
112+ Q_ASSERT(m_modelState == IdleState);
113+ m_modelState = ResettingState;
114+
115+ beginResetModel();
116+
100117 if (m_surfaceManager) {
101118 disconnect(m_surfaceManager, 0, this, 0);
102119 }
@@ -104,13 +121,16 @@ void TopLevelWindowModel::setSurfaceManager(unityapi::SurfaceManagerInterface *s
104121 m_surfaceManager = surfaceManager;
105122
106123 if (m_surfaceManager) {
107− connect(m_surfaceManager, &unityapi::SurfaceManagerInterface::surfaceCreated, this, &TopLevelWindowModel::onSurfaceCreated);
124+ connect(m_surfaceManager, &unityapi::SurfaceManagerInterface::surfacesAddedToWorkspace, this, &TopLevelWindowModel::onSurfacesAddedToWorkspace);
108125 connect(m_surfaceManager, &unityapi::SurfaceManagerInterface::surfacesRaised, this, &TopLevelWindowModel::onSurfacesRaised);
109126 connect(m_surfaceManager, &unityapi::SurfaceManagerInterface::modificationsStarted, this, &TopLevelWindowModel::onModificationsStarted);
110127 connect(m_surfaceManager, &unityapi::SurfaceManagerInterface::modificationsEnded, this, &TopLevelWindowModel::onModificationsEnded);
111128 }
112129
113− Q_EMIT surfaceManagerChanged(m_surfaceManager);
130+ refreshWindows();
131+
132+ endResetModel();
133+ m_modelState = IdleState;
114134}
115135
116136void TopLevelWindowModel::addApplication(unityapi::ApplicationInfoInterface *application)
@@ -150,6 +170,7 @@ void TopLevelWindowModel::prependSurface(unityapi::MirSurfaceInterface *surface,
150170 Q_ASSERT(surface != nullptr);
151171
152172 connectSurface(surface);
173+ m_allSurfaces.insert(surface);
153174
154175 bool filledPlaceholder = false;
155176 for (int i = 0; i < m_windowModel.count() && !filledPlaceholder; ++i) {
@@ -226,15 +247,14 @@ void TopLevelWindowModel::connectWindow(Window *window)
226247
227248 connect(window, &Window::focusedChanged, this, [this, window](bool focused) {
228249 if (window->surface()) {
229− // Condense changes to the focused window
230− // eg: Do focusedWindow=A to focusedWindow=B instead of
231− // focusedWindow=A to focusedWindow=null to focusedWindow=B
232250 if (focused) {
233− Q_ASSERT(m_newlyFocusedWindow == nullptr);
234− m_focusedWindowChanged = true;
235− m_newlyFocusedWindow = window;
251+ setFocusedWindow(window);
252+ m_focusedWindowCleared = false;
236253 } else if (m_focusedWindow == window) {
237− m_focusedWindowChanged = true;
254+ // Condense changes to the focused window
255+ // eg: Do focusedWindow=A to focusedWindow=B instead of
256+ // focusedWindow=A to focusedWindow=null to focusedWindow=B
257+ m_focusedWindowCleared = true;
238258 } else {
239259 // don't clear the focused window if you were not there in the first place
240260 // happens when a filled window gets replaced with an empty one (no surface) as the focused window.
@@ -339,6 +359,7 @@ void TopLevelWindowModel::onSurfaceDestroyed(unityapi::MirSurfaceInterface *surf
339359 auto window = m_windowModel[i].window;
340360 window->setSurface(nullptr);
341361 window->setFocused(false);
362+ m_allSurfaces.remove(surface);
342363 INFO_MSG << " Removed surface from entry. After: " << toString();
343364 }
344365}
@@ -354,48 +375,107 @@ Window *TopLevelWindowModel::createWindow(unityapi::MirSurfaceInterface *surface
354375 return qmlWindow;
355376}
356377
357−void TopLevelWindowModel::onSurfaceCreated(unityapi::MirSurfaceInterface *surface)
378+void TopLevelWindowModel::onSurfacesAddedToWorkspace(const std::shared_ptr<miral::Workspace>& workspace,
379+ const QVector<unity::shell::application::MirSurfaceInterface*> surfaces)
358380{
359− DEBUG_MSG << "(" << surface << ")";
381+ if (!m_workspace || !m_applicationManager) return;
382+ if (workspace != m_workspace->workspace()) {
383+ removeSurfaces(surfaces);
384+ return;
385+ }
360386
361− if (surface->parentSurface()) {
362− // Wrap it in a Window so that we keep focusedWindow() up to date.
363− Window *window = createWindow(surface);
364− connect(surface, &QObject::destroyed, window, [=](){
365− window->setSurface(nullptr);
366− window->deleteLater();
367− });
368− } else {
369− if (surface->type() == Mir::InputMethodType) {
370− connectSurface(surface);
371− setInputMethodWindow(createWindow(surface));
387+ Q_FOREACH(auto surface, surfaces) {
388+ if (m_allSurfaces.contains(surface)) continue;
389+
390+ if (surface->parentSurface()) {
391+ // Wrap it in a Window so that we keep focusedWindow() up to date.
392+ Window *window = createWindow(surface);
393+ connect(surface, &QObject::destroyed, window, [=](){
394+ window->setSurface(nullptr);
395+ window->deleteLater();
396+ });
372397 } else {
373− auto *application = m_applicationManager->findApplicationWithSurface(surface);
374− if (application) {
375− if (surface->state() == Mir::HiddenState) {
376− // Ignore it until it's finally shown
377− connect(surface, &unityapi::MirSurfaceInterface::stateChanged, this, [=](Mir::State newState) {
378− Q_ASSERT(newState != Mir::HiddenState);
379− disconnect(surface, &unityapi::MirSurfaceInterface::stateChanged, this, 0);
398+ if (surface->type() == Mir::InputMethodType) {
399+ connectSurface(surface);
400+ setInputMethodWindow(createWindow(surface));
401+ } else {
402+ auto *application = m_applicationManager->findApplicationWithSurface(surface);
403+ if (application) {
404+ if (surface->state() == Mir::HiddenState) {
405+ // Ignore it until it's finally shown
406+ connect(surface, &unityapi::MirSurfaceInterface::stateChanged, this, [=](Mir::State newState) {
407+ Q_ASSERT(newState != Mir::HiddenState);
408+ disconnect(surface, &unityapi::MirSurfaceInterface::stateChanged, this, 0);
409+ prependSurface(surface, application);
410+ });
411+ } else {
380412 prependSurface(surface, application);
381− });
413+ }
382414 } else {
383− prependSurface(surface, application);
415+ // Must be a prompt session. No need to do add it as a prompt surface is not top-level.
416+ // It will show up in the ApplicationInfoInterface::promptSurfaceList of some application.
417+ // Still wrap it in a Window though, so that we keep focusedWindow() up to date.
418+ Window *promptWindow = createWindow(surface);
419+ connect(surface, &QObject::destroyed, promptWindow, [=](){
420+ promptWindow->setSurface(nullptr);
421+ promptWindow->deleteLater();
422+ });
384423 }
385− } else {
386− // Must be a prompt session. No need to do add it as a prompt surface is not top-level.
387− // It will show up in the ApplicationInfoInterface::promptSurfaceList of some application.
388− // Still wrap it in a Window though, so that we keep focusedWindow() up to date.
389− Window *promptWindow = createWindow(surface);
390− connect(surface, &QObject::destroyed, promptWindow, [=](){
391− promptWindow->setSurface(nullptr);
392− promptWindow->deleteLater();
393− });
394424 }
395425 }
396426 }
397427}
398428
429+void TopLevelWindowModel::removeSurfaces(const QVector<unity::shell::application::MirSurfaceInterface *> surfaces)
430+{
431+ int start = -1;
432+ int end = -1;
433+ for (auto iter = surfaces.constBegin(); iter != surfaces.constEnd();) {
434+ auto surface = *iter;
435+ iter++;
436+
437+ // Do removals in adjacent blocks.
438+ start = end = indexOf(surface);
439+ if (start == -1) {
440+ // could be a child surface
441+ m_allSurfaces.remove(surface);
442+ continue;
443+ }
444+ while(iter != surfaces.constEnd()) {
445+ int index = indexOf(*iter);
446+ if (index != end+1) {
447+ break;
448+ }
449+ end++;
450+ iter++;
451+ }
452+
453+ if (m_modelState == IdleState) {
454+ beginRemoveRows(QModelIndex(), start, end);
455+ m_modelState = RemovingState;
456+ } else {
457+ Q_ASSERT(m_modelState == ResettingState);
458+ // No point in signaling anything if we're resetting the whole model
459+ }
460+
461+ for (int index = start; index <= end; index++) {
462+ auto window = m_windowModel[start].window;
463+ window->setSurface(nullptr);
464+ window->setFocused(false);
465+
466+ m_windowModel.removeAt(start);
467+ m_allSurfaces.remove(surface);
468+ }
469+
470+ if (m_modelState == RemovingState) {
471+ endRemoveRows();
472+ Q_EMIT countChanged();
473+ Q_EMIT listChanged();
474+ m_modelState = IdleState;
475+ }
476+ }
477+}
478+
399479void TopLevelWindowModel::deleteAt(int index)
400480{
401481 auto window = m_windowModel[index].window;
@@ -418,12 +498,14 @@ void TopLevelWindowModel::removeAt(int index)
418498 }
419499
420500 auto window = m_windowModel[index].window;
501+ auto surface = window->surface();
421502
422503 if (!window->surface()) {
423504 window->setFocused(false);
424505 }
425506
426507 m_windowModel.removeAt(index);
508+ m_allSurfaces.remove(surface);
427509
428510 if (m_modelState == RemovingState) {
429511 endRemoveRows();
@@ -434,6 +516,7 @@ void TopLevelWindowModel::removeAt(int index)
434516
435517 if (m_focusedWindow == window) {
436518 setFocusedWindow(nullptr);
519+ m_focusedWindowCleared = false;
437520 }
438521
439522 INFO_MSG << " after " << toString();
@@ -452,6 +535,15 @@ void TopLevelWindowModel::setInputMethodWindow(Window *window)
452535void TopLevelWindowModel::removeInputMethodWindow()
453536{
454537 if (m_inputMethodWindow) {
538+ auto surface = m_inputMethodWindow->surface();
539+ if (surface) {
540+ m_allSurfaces.remove(surface);
541+ }
542+ if (m_focusedWindow == m_inputMethodWindow) {
543+ setFocusedWindow(nullptr);
544+ m_focusedWindowCleared = false;
545+ }
546+
455547 delete m_inputMethodWindow;
456548 m_inputMethodWindow = nullptr;
457549 Q_EMIT inputMethodSurfaceChanged(nullptr);
@@ -463,7 +555,7 @@ void TopLevelWindowModel::onSurfacesRaised(const QVector<unityapi::MirSurfaceInt
463555 DEBUG_MSG << "(" << surfaces << ")";
464556 const int raiseCount = surfaces.size();
465557 for (int i = 0; i < raiseCount; i++) {
466− int fromIndex = findIndexOf(surfaces[i]);
558+ int fromIndex = indexOf(surfaces[i]);
467559 if (fromIndex != -1) {
468560 move(fromIndex, 0);
469561 }
@@ -490,16 +582,6 @@ QVariant TopLevelWindowModel::data(const QModelIndex& index, int role) const
490582 }
491583}
492584
493−int TopLevelWindowModel::findIndexOf(const unityapi::MirSurfaceInterface *surface) const
494−{
495− for (int i=0; i<m_windowModel.count(); i++) {
496− if (m_windowModel[i].window->surface() == surface) {
497− return i;
498− }
499− }
500− return -1;
501−}
502−
503585int TopLevelWindowModel::generateId()
504586{
505587 int id = m_nextId;
@@ -701,12 +783,11 @@ void TopLevelWindowModel::onModificationsStarted()
701783
702784void TopLevelWindowModel::onModificationsEnded()
703785{
704− if (m_focusedWindowChanged) {
705− setFocusedWindow(m_newlyFocusedWindow);
786+ if (m_focusedWindowCleared) {
787+ setFocusedWindow(nullptr);
706788 }
707789 // reset
708− m_focusedWindowChanged = false;
709− m_newlyFocusedWindow = nullptr;
790+ m_focusedWindowCleared = false;
710791}
711792
712793void TopLevelWindowModel::activateTopMostWindowWithoutId(int forbiddenId)
@@ -725,4 +806,55 @@ void TopLevelWindowModel::activateNullWindow()
725806{
726807 if (!m_nullWindow->focused())
727808 m_nullWindow->activate();
728−}
809+}
810+
811+void TopLevelWindowModel::refreshWindows()
812+{
813+ DEBUG_MSG << "()";
814+ clear();
815+
816+ if (!m_workspace || !m_applicationManager || !m_surfaceManager) return;
817+
818+ m_surfaceManager->forEachSurfaceInWorkspace(m_workspace->workspace(), [this](unity::shell::application::MirSurfaceInterface* surface) {
819+ if (surface->parentSurface()) {
820+ // Wrap it in a Window so that we keep focusedWindow() up to date.
821+ Window *window = createWindow(surface);
822+ connect(surface, &QObject::destroyed, window, [=](){
823+ window->setSurface(nullptr);
824+ window->deleteLater();
825+ });
826+ } else {
827+ if (surface->type() == Mir::InputMethodType) {
828+ setInputMethodWindow(createWindow(surface));
829+ } else {
830+ auto *application = m_applicationManager->findApplicationWithSurface(surface);
831+ if (application) {
832+ prependSurface(surface, application);
833+ } else {
834+ // Must be a prompt session. No need to do add it as a prompt surface is not top-level.
835+ // It will show up in the ApplicationInfoInterface::promptSurfaceList of some application.
836+ // Still wrap it in a Window though, so that we keep focusedWindow() up to date.
837+ Window *promptWindow = createWindow(surface);
838+ connect(surface, &QObject::destroyed, promptWindow, [=](){
839+ promptWindow->setSurface(nullptr);
840+ promptWindow->deleteLater();
841+ });
842+ }
843+ }
844+ }
845+ });
846+}
847+
848+void TopLevelWindowModel::clear()
849+{
850+ DEBUG_MSG << "()";
851+
852+ while(m_windowModel.count() > 0) {
853+ ModelEntry entry = m_windowModel.takeAt(0);
854+ disconnect(entry.window, 0, this, 0);
855+ delete entry.window;
856+ }
857+ m_allSurfaces.clear();
858+ setFocusedWindow(nullptr);
859+ m_focusedWindowCleared = false;
860+}
plugins/WindowManager/TopLevelWindowModel.h+19−24
@@ -20,11 +20,16 @@
2020#include <QAbstractListModel>
2121#include <QLoggingCategory>
2222
23+#include <memory>
24+
2325#include "WindowManagerGlobal.h"
2426
2527Q_DECLARE_LOGGING_CATEGORY(TOPLEVELWINDOWMODEL)
2628
2729class Window;
30+class Workspace;
31+
32+namespace miral { class Workspace; }
2833
2934namespace unity {
3035 namespace shell {
@@ -72,16 +77,6 @@ class WINDOWMANAGERQML_EXPORT TopLevelWindowModel : public QAbstractListModel
7277 */
7378 Q_PROPERTY(Window* focusedWindow READ focusedWindow NOTIFY focusedWindowChanged)
7479
75− Q_PROPERTY(unity::shell::application::SurfaceManagerInterface* surfaceManager
76− READ surfaceManager
77− WRITE setSurfaceManager
78− NOTIFY surfaceManagerChanged)
79−
80− Q_PROPERTY(unity::shell::application::ApplicationManagerInterface* applicationManager
81− READ applicationManager
82− WRITE setApplicationManager
83− NOTIFY applicationManagerChanged)
84−
8580 /**
8681 The id to be used on the next entry created
8782 Useful for tests
@@ -100,7 +95,8 @@ public:
10095 ApplicationRole = Qt::UserRole + 1,
10196 };
10297
103− TopLevelWindowModel();
98+ TopLevelWindowModel(Workspace* workspace);
99+ ~TopLevelWindowModel();
104100
105101 // From QAbstractItemModel
106102 int rowCount(const QModelIndex &parent = QModelIndex()) const override;
@@ -111,17 +107,11 @@ public:
111107 return roleNames;
112108 }
113109
114− // Own API
110+ // Own API;
115111
116112 unity::shell::application::MirSurfaceInterface* inputMethodSurface() const;
117113 Window* focusedWindow() const;
118114
119− unity::shell::application::ApplicationManagerInterface *applicationManager() const { return m_applicationManager; }
120− void setApplicationManager(unity::shell::application::ApplicationManagerInterface*);
121−
122− unity::shell::application::SurfaceManagerInterface *surfaceManager() const { return m_surfaceManager; }
123− void setSurfaceManager(unity::shell::application::SurfaceManagerInterface*);
124−
125115 int nextId() const { return m_nextId; }
126116
127117public:
@@ -168,13 +158,13 @@ public:
168158 * @brief Raises and focuses a window with no surface
169159 */
170160 Q_INVOKABLE void activateNullWindow();
161+ void setApplicationManager(unity::shell::application::ApplicationManagerInterface*);
162+ void setSurfaceManager(unity::shell::application::SurfaceManagerInterface*);
171163
172164Q_SIGNALS:
173165 void countChanged();
174166 void inputMethodSurfaceChanged(unity::shell::application::MirSurfaceInterface* inputMethodSurface);
175167 void focusedWindowChanged(Window *focusedWindow);
176− void applicationManagerChanged(unity::shell::application::ApplicationManagerInterface*);
177− void surfaceManagerChanged(unity::shell::application::SurfaceManagerInterface*);
178168
179169 /**
180170 * @brief Emitted when the list changes
@@ -186,7 +176,8 @@ Q_SIGNALS:
186176 void nextIdChanged();
187177
188178private Q_SLOTS:
189− void onSurfaceCreated(unity::shell::application::MirSurfaceInterface *surface);
179+ void onSurfacesAddedToWorkspace(const std::shared_ptr<miral::Workspace>& workspace,
180+ const QVector<unity::shell::application::MirSurfaceInterface*> surfaces);
190181 void onSurfacesRaised(const QVector<unity::shell::application::MirSurfaceInterface*> &surfaces);
191182
192183 void onModificationsStarted();
@@ -203,9 +194,9 @@ private:
203194 void setInputMethodWindow(Window *window);
204195 void setFocusedWindow(Window *window);
205196 void removeInputMethodWindow();
206− int findIndexOf(const unity::shell::application::MirSurfaceInterface *surface) const;
207197 void deleteAt(int index);
208198 void removeAt(int index);
199+ void removeSurfaces(const QVector<unity::shell::application::MirSurfaceInterface *> surfaces);
209200
210201 void addApplication(unity::shell::application::ApplicationInfoInterface *application);
211202 void removeApplication(unity::shell::application::ApplicationInfoInterface *application);
@@ -228,6 +219,8 @@ private:
228219 void activateEmptyWindow(Window *window);
229220
230221 void activateTopMostWindowWithoutId(int forbiddenId);
222+ void refreshWindows();
223+ void clear();
231224
232225 Window *createWindow(unity::shell::application::MirSurfaceInterface *surface);
233226
@@ -245,6 +238,9 @@ private:
245238 Window* m_inputMethodWindow{nullptr};
246239 Window* m_focusedWindow{nullptr};
247240 Window* m_nullWindow;
241+ Workspace* m_workspace{nullptr};
242+ // track all the surfaces we've been told about.
243+ QSet<unity::shell::application::MirSurfaceInterface*> m_allSurfaces;
248244
249245 int m_nextId{1};
250246 // Just something big enough that we don't risk running out of unused id numbers.
@@ -265,8 +261,7 @@ private:
265261 ModelState m_modelState{IdleState};
266262
267263 // Valid between modificationsStarted and modificationsEnded
268− bool m_focusedWindowChanged{false};
269− Window *m_newlyFocusedWindow{nullptr};
264+ bool m_focusedWindowCleared{false};
270265};
271266
272267#endif // TOPLEVELWINDOWMODEL_H
plugins/WindowManager/WindowManagerObjects.cppnew file+47
@@ -0,0 +1,47 @@
1+/*
2+ * Copyright (C) 2017 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+#include "WindowManagerObjects.h"
18+
19+WindowManagerObjects::WindowManagerObjects(QObject *parent)
20+ : QObject(parent)
21+ , m_surfaceManager(nullptr)
22+ , m_applicationManager(nullptr)
23+{
24+}
25+
26+WindowManagerObjects *WindowManagerObjects::instance()
27+{
28+ static WindowManagerObjects* objects(new WindowManagerObjects());
29+ return objects;
30+}
31+
32+
33+void WindowManagerObjects::setSurfaceManager(unity::shell::application::SurfaceManagerInterface *surfaceManager)
34+{
35+ if (m_surfaceManager == surfaceManager) return;
36+
37+ m_surfaceManager = surfaceManager;
38+ Q_EMIT surfaceManagerChanged(surfaceManager);
39+}
40+
41+void WindowManagerObjects::setApplicationManager(unity::shell::application::ApplicationManagerInterface *applicationManager)
42+{
43+ if (m_applicationManager == applicationManager) return;
44+
45+ m_applicationManager = applicationManager;
46+ Q_EMIT applicationManagerChanged(applicationManager);
47+}
plugins/WindowManager/WindowManagerObjects.hnew file+66
@@ -0,0 +1,66 @@
1+/*
2+ * Copyright (C) 2017 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+#ifndef WINDOWMANAGEROBJECTS_H
18+#define WINDOWMANAGEROBJECTS_H
19+
20+#include <QObject>
21+
22+#include "WindowManagerGlobal.h"
23+
24+namespace unity {
25+ namespace shell {
26+ namespace application {
27+ class SurfaceManagerInterface;
28+ class ApplicationManagerInterface;
29+ }
30+ }
31+}
32+
33+class WINDOWMANAGERQML_EXPORT WindowManagerObjects : public QObject
34+{
35+ Q_OBJECT
36+
37+ Q_PROPERTY(unity::shell::application::SurfaceManagerInterface* surfaceManager
38+ READ surfaceManager
39+ WRITE setSurfaceManager
40+ NOTIFY surfaceManagerChanged)
41+
42+ Q_PROPERTY(unity::shell::application::ApplicationManagerInterface* applicationManager
43+ READ applicationManager
44+ WRITE setApplicationManager
45+ NOTIFY applicationManagerChanged)
46+public:
47+ explicit WindowManagerObjects(QObject *parent = 0);
48+
49+ static WindowManagerObjects *instance();
50+
51+ unity::shell::application::SurfaceManagerInterface *surfaceManager() const { return m_surfaceManager; }
52+ void setSurfaceManager(unity::shell::application::SurfaceManagerInterface*);
53+
54+ unity::shell::application::ApplicationManagerInterface *applicationManager() const { return m_applicationManager; }
55+ void setApplicationManager(unity::shell::application::ApplicationManagerInterface*);
56+
57+Q_SIGNALS:
58+ void surfaceManagerChanged(unity::shell::application::SurfaceManagerInterface*);
59+ void applicationManagerChanged(unity::shell::application::ApplicationManagerInterface*);
60+
61+private:
62+ unity::shell::application::SurfaceManagerInterface* m_surfaceManager;
63+ unity::shell::application::ApplicationManagerInterface* m_applicationManager;
64+};
65+
66+#endif // WINDOWMANAGEROBJECTS_H
plugins/WindowManager/WindowManagerPlugin.cpp+57−2
@@ -17,19 +17,74 @@
1717#include "WindowManagerPlugin.h"
1818
1919#include "AvailableDesktopArea.h"
20+#include "Screen.h"
21+#include "ScreenAttached.h"
22+#include "Screens.h"
23+#include "ScreensConfiguration.h"
24+#include "ScreenWindow.h"
2025#include "TopLevelWindowModel.h"
2126#include "Window.h"
27+#include "WindowManagerObjects.h"
2228#include "WindowMargins.h"
29+#include "WorkspaceManager.h"
30+#include "Workspace.h"
31+#include "WorkspaceModel.h"
2332
2433#include <QtQml>
34+#include <qtmir/qtmir.h>
35+
36+namespace {
37+
38+static const QString notInstantiatable = QStringLiteral("Not instantiatable");
39+
40+static QObject *workspace_manager(QQmlEngine *engine, QJSEngine *scriptEngine)
41+{
42+ Q_UNUSED(engine)
43+ Q_UNUSED(scriptEngine)
44+ return WorkspaceManager::instance();
45+}
46+QObject* screensSingleton(QQmlEngine* engine, QJSEngine* scriptEngine) {
47+ Q_UNUSED(engine);
48+ Q_UNUSED(scriptEngine);
49+ return ConcreteScreens::self();
50+}
51+QObject* objectsSingleton(QQmlEngine* engine, QJSEngine* scriptEngine) {
52+ Q_UNUSED(engine);
53+ Q_UNUSED(scriptEngine);
54+ return WindowManagerObjects::instance();
55+}
56+
57+} // namspace
2558
2659void WindowManagerPlugin::registerTypes(const char *uri)
2760{
2861 qmlRegisterType<AvailableDesktopArea>(uri, 1, 0, "AvailableDesktopArea");
29− qmlRegisterType<TopLevelWindowModel>(uri, 1, 0, "TopLevelWindowModel");
3062 qmlRegisterType<WindowMargins>(uri, 1, 0, "WindowMargins");
63+ qmlRegisterSingletonType<WorkspaceManager>(uri, 1, 0, "WorkspaceManager", workspace_manager);
64+ qmlRegisterSingletonType<ConcreteScreens>(uri, 1, 0, "Screens", screensSingleton);
65+ qmlRegisterUncreatableType<qtmir::ScreenMode>(uri, 1, 0, "ScreenMode", notInstantiatable);
66+ qmlRegisterSingletonType<WindowManagerObjects>(uri, 1, 0, "WindowManagerObjects", objectsSingleton);
3167
32− qRegisterMetaType<Window*>("Window*");
68+ qRegisterMetaType<ConcreteScreen*>("ConcreteScreen*");
69+ qRegisterMetaType<ProxyScreens*>("ProxyScreens*");
70+ qRegisterMetaType<Workspace*>("Workspace*");
71+ qRegisterMetaType<TopLevelWindowModel*>("TopLevelWindowModel*");
72+ qRegisterMetaType<ScreenConfig*>("ScreenConfig*");
73+ qRegisterMetaType<WorkspaceModel*>("WorkspaceModel*");
3374
75+ qRegisterMetaType<Window*>("Window*");
3476 qRegisterMetaType<QAbstractListModel*>("QAbstractListModel*");
77+
78+ qmlRegisterType<ScreenWindow>(uri, 1, 0, "ScreenWindow");
79+ qmlRegisterRevision<QWindow,1>(uri, 1, 0);
80+
81+ qmlRegisterUncreatableType<WMScreen>(uri, 1, 0, "WMScreen", notInstantiatable);
82+}
83+
84+void WindowManagerPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
85+{
86+ QQmlExtensionPlugin::initializeEngine(engine, uri);
87+
88+ // Create Screens
89+ new ConcreteScreens(qtmir::get_screen_model(), new ScreensConfiguration());
3590}
plugins/WindowManager/WindowManagerPlugin.h+1
@@ -27,6 +27,7 @@ class WindowManagerPlugin : public QQmlExtensionPlugin
2727
2828public:
2929 void registerTypes(const char *uri) override;
30+ void initializeEngine(QQmlEngine *engine, const char *uri) override;
3031};
3132
3233#endif // WINDOWMANAGER_PLUGIN_H
plugins/WindowManager/Workspace.cppnew file+178
@@ -0,0 +1,178 @@
1+/*
2+ * Copyright (C) 2017 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+#include "Workspace.h"
18+#include "WorkspaceModel.h"
19+#include "WorkspaceManager.h"
20+#include "TopLevelWindowModel.h"
21+#include "Screen.h"
22+
23+#include "wmpolicyinterface.h"
24+
25+int nextWorkspace = 0;
26+
27+Workspace::Workspace(QObject *parent)
28+ : QObject(parent)
29+ , m_workspace(WMPolicyInterface::instance()->createWorkspace())
30+ , m_model(nullptr)
31+{
32+ setObjectName((QString("Wks%1").arg(nextWorkspace++)));
33+}
34+
35+Workspace::Workspace(const Workspace &other)
36+ : QObject(nullptr)
37+ , m_workspace(other.m_workspace)
38+ , m_model(nullptr)
39+{
40+ setObjectName(other.objectName());
41+
42+ connect(&other, &Workspace::activeChanged, this, &Workspace::activeChanged);
43+}
44+
45+Workspace::~Workspace()
46+{
47+ if (m_model) {
48+ m_model->remove(this);
49+ }
50+}
51+
52+void Workspace::assign(WorkspaceModel *model, const QVariant& vIndex)
53+{
54+ if (m_model == model) return;
55+
56+ if (m_model) {
57+ disconnect(m_model, 0, this, 0);
58+ m_model->remove(this);
59+ }
60+
61+ m_model = model;
62+
63+ if (model) {
64+ int index = m_model->rowCount();
65+ if (vIndex.isValid() && vIndex.canConvert(QVariant::Int)) {
66+ index = vIndex.toInt();
67+ }
68+ m_model->insert(index, this);
69+
70+ connect(m_model, &QObject::destroyed, this, [this]() {
71+ m_model = nullptr;
72+ Q_EMIT unassigned();
73+ });
74+ Q_EMIT assigned();
75+ } else {
76+ Q_EMIT unassigned();
77+ }
78+}
79+
80+void Workspace::unassign()
81+{
82+ assign(nullptr);
83+}
84+
85+bool Workspace::isAssigned() const
86+{
87+ return m_model != nullptr;
88+}
89+
90+bool Workspace::isSameAs(Workspace *wks) const
91+{
92+ if (!wks) return false;
93+ if (wks == this) return true;
94+ return wks->workspace() == workspace();
95+}
96+
97+
98+ConcreteWorkspace::ConcreteWorkspace(QObject *parent)
99+ : Workspace(parent)
100+ , m_active(false)
101+ , m_windowModel(new TopLevelWindowModel(this))
102+{
103+ connect(WorkspaceManager::instance(), &WorkspaceManager::activeWorkspaceChanged, this, [this](Workspace* activeWorkspace) {
104+ bool newActive = activeWorkspace == this;
105+ if (newActive != m_active) {
106+ m_active = newActive;
107+ Q_EMIT activeChanged(m_active);
108+
109+ if (m_active) {
110+ WMPolicyInterface::instance()->setActiveWorkspace(m_workspace);
111+ }
112+ }
113+ });
114+}
115+
116+ConcreteWorkspace::~ConcreteWorkspace()
117+{
118+ WorkspaceManager::instance()->destroyWorkspace(this);
119+ WMPolicyInterface::instance()->releaseWorkspace(m_workspace);
120+}
121+
122+TopLevelWindowModel *ConcreteWorkspace::windowModel() const
123+{
124+ return m_windowModel.data();
125+}
126+
127+void ConcreteWorkspace::activate()
128+{
129+ WorkspaceManager::instance()->setActiveWorkspace(this);
130+}
131+
132+void ConcreteWorkspace::setCurrentOn(Screen *screen)
133+{
134+ if (screen) {
135+ screen->setCurrentWorkspace(this);
136+ }
137+}
138+
139+
140+ProxyWorkspace::ProxyWorkspace(Workspace * const workspace)
141+ : Workspace(*workspace)
142+ , m_original(workspace)
143+{
144+}
145+
146+void ProxyWorkspace::assign(WorkspaceModel *model, const QVariant &index)
147+{
148+ Workspace::assign(model, index);
149+}
150+
151+void ProxyWorkspace::unassign()
152+{
153+ Workspace::unassign();
154+}
155+
156+bool ProxyWorkspace::isActive() const
157+{
158+ return m_original ? m_original->isActive() : false;
159+}
160+
161+TopLevelWindowModel *ProxyWorkspace::windowModel() const
162+{
163+ return m_original ? m_original->windowModel() : nullptr;
164+}
165+
166+void ProxyWorkspace::activate()
167+{
168+ if (m_original) {
169+ m_original->activate();
170+ }
171+}
172+
173+void ProxyWorkspace::setCurrentOn(Screen *screen)
174+{
175+ if (screen && m_original) {
176+ screen->setCurrentWorkspace(m_original);
177+ }
178+}
plugins/WindowManager/Workspace.hnew file+122
@@ -0,0 +1,122 @@
1+/*
2+ * Copyright (C) 2017 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+#ifndef WINDOWMANAGER_WORKSPACE_H
18+#define WINDOWMANAGER_WORKSPACE_H
19+
20+#include <QObject>
21+#include <QVariant>
22+#include <QPointer>
23+#include <QSharedPointer>
24+
25+#include <memory>
26+#include <functional>
27+
28+#include "WindowManagerGlobal.h"
29+
30+class WorkspaceModel;
31+class TopLevelWindowModel;
32+class Screen;
33+
34+namespace miral { class Workspace; }
35+
36+namespace unity {
37+ namespace shell {
38+ namespace application {
39+ class MirSurfaceInterface;
40+ }
41+ }
42+}
43+
44+class WINDOWMANAGERQML_EXPORT Workspace : public QObject
45+{
46+ Q_OBJECT
47+ Q_PROPERTY(bool active READ isActive NOTIFY activeChanged)
48+ Q_PROPERTY(TopLevelWindowModel* windowModel READ windowModel CONSTANT)
49+public:
50+ virtual ~Workspace();
51+
52+ virtual void assign(WorkspaceModel* model, const QVariant& index = QVariant());
53+ virtual void unassign();
54+
55+ virtual bool isActive() const = 0;
56+ virtual TopLevelWindowModel *windowModel() const = 0;
57+ virtual void setCurrentOn(Screen*) = 0;
58+
59+ std::shared_ptr<miral::Workspace> workspace() const { return m_workspace; }
60+ bool isAssigned() const;
61+ Q_INVOKABLE bool isSameAs(Workspace*) const;
62+
63+public Q_SLOTS:
64+ virtual void activate() = 0;
65+
66+Q_SIGNALS:
67+ void assigned();
68+ void unassigned();
69+
70+ void activeChanged(bool);
71+
72+protected:
73+ Workspace(QObject *parent = nullptr);
74+ Workspace(Workspace const& other);
75+
76+ std::shared_ptr<miral::Workspace> m_workspace;
77+ WorkspaceModel* m_model;
78+};
79+
80+class WINDOWMANAGERQML_EXPORT ConcreteWorkspace : public Workspace
81+{
82+public:
83+ ~ConcreteWorkspace();
84+
85+ bool isActive() const override { return m_active; }
86+ TopLevelWindowModel *windowModel() const override;
87+ void activate() override;
88+ void setCurrentOn(Screen*) override;
89+
90+private:
91+ explicit ConcreteWorkspace(QObject *parent = nullptr);
92+
93+ bool m_active;
94+ const QScopedPointer<TopLevelWindowModel> m_windowModel;
95+
96+ friend class WorkspaceManager;
97+};
98+
99+class ProxyWorkspace : public Workspace
100+{
101+ Q_OBJECT
102+public:
103+ explicit ProxyWorkspace(Workspace*const workspace);
104+ ~ProxyWorkspace() = default;
105+
106+ Q_INVOKABLE void assign(WorkspaceModel* model, const QVariant& index = QVariant()) override;
107+
108+ bool isActive() const override;
109+ TopLevelWindowModel *windowModel() const override;
110+ void activate() override;
111+ void setCurrentOn(Screen*) override;
112+
113+ Workspace* proxyObject() const { return m_original.data(); }
114+
115+public Q_SLOTS:
116+ void unassign() override;
117+
118+private:
119+ const QPointer<Workspace> m_original;
120+};
121+
122+#endif // WINDOWMANAGER_WORKSPACE_H
plugins/WindowManager/WorkspaceManager.cppnew file+106
@@ -0,0 +1,106 @@
1+/*
2+ * Copyright (C) 2017 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+#include "WorkspaceManager.h"
18+#include "Workspace.h"
19+#include "TopLevelWindowModel.h"
20+#include "WindowManagerObjects.h"
21+#include <unity/shell/application/SurfaceManagerInterface.h>
22+
23+// Qt
24+#include <QGuiApplication>
25+#include <QScreen>
26+#include <QQmlEngine>
27+
28+WorkspaceManager *WorkspaceManager::instance()
29+{
30+ static WorkspaceManager* workspaceManager(new WorkspaceManager());
31+ return workspaceManager;
32+}
33+
34+WorkspaceManager::WorkspaceManager()
35+ : m_activeWorkspace(nullptr)
36+{
37+}
38+
39+Workspace *WorkspaceManager::createWorkspace()
40+{
41+ auto workspace = new ConcreteWorkspace(this);
42+ QQmlEngine::setObjectOwnership(workspace, QQmlEngine::CppOwnership);
43+ m_allWorkspaces.insert(workspace);
44+
45+ if (m_allWorkspaces.count() == 0 && m_activeWorkspace) {
46+ setActiveWorkspace(nullptr);
47+ } else if (m_allWorkspaces.count() == 1) {
48+ setActiveWorkspace(workspace);
49+ }
50+
51+ return workspace;
52+}
53+
54+void WorkspaceManager::destroyWorkspace(Workspace *workspace)
55+{
56+ if (!workspace) return;
57+
58+ if (workspace->isAssigned()) {
59+ workspace->unassign();
60+ }
61+ m_allWorkspaces.remove(workspace);
62+
63+ if (m_activeWorkspace == workspace) {
64+ setActiveWorkspace(m_allWorkspaces.count() ? *m_allWorkspaces.begin() : nullptr);
65+ }
66+ if (m_activeWorkspace) {
67+ moveWorkspaceContentToWorkspace(m_activeWorkspace, workspace);
68+ }
69+
70+ disconnect(workspace, 0, this, 0);
71+}
72+
73+void WorkspaceManager::moveSurfaceToWorkspace(unity::shell::application::MirSurfaceInterface *surface, Workspace *workspace)
74+{
75+ auto surfaceManager = WindowManagerObjects::instance()->surfaceManager();
76+ if (surfaceManager) {
77+ surfaceManager->moveSurfaceToWorkspace(surface, workspace->workspace());
78+ }
79+}
80+
81+void WorkspaceManager::moveWorkspaceContentToWorkspace(Workspace *to, Workspace *from)
82+{
83+ auto surfaceManager = WindowManagerObjects::instance()->surfaceManager();
84+ if (surfaceManager) {
85+ surfaceManager->moveWorkspaceContentToWorkspace(to->workspace(), from->workspace());
86+ }
87+}
88+
89+Workspace *WorkspaceManager::activeWorkspace() const
90+{
91+ return m_activeWorkspace;
92+}
93+
94+void WorkspaceManager::setActiveWorkspace(Workspace *workspace)
95+{
96+ if (workspace != m_activeWorkspace) {
97+ m_activeWorkspace = workspace;
98+ Q_EMIT activeWorkspaceChanged(workspace);
99+ }
100+}
101+
102+void WorkspaceManager::setActiveWorkspace2(Workspace *workspace)
103+{
104+ if (!workspace) return;
105+ workspace->activate();
106+}
plugins/WindowManager/WorkspaceManager.hnew file+69
@@ -0,0 +1,69 @@
1+/*
2+ * Copyright (C) 2017 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+#ifndef WORKSPACEMANAGER_H
18+#define WORKSPACEMANAGER_H
19+
20+#include <QQmlListProperty>
21+
22+#include "WindowManagerGlobal.h"
23+#include "WorkspaceModel.h"
24+
25+class Workspace;
26+
27+namespace unity {
28+ namespace shell {
29+ namespace application {
30+ class MirSurfaceInterface;
31+ class SurfaceManagerInterface;
32+ }
33+ }
34+}
35+
36+class WINDOWMANAGERQML_EXPORT WorkspaceManager : public QObject
37+{
38+ Q_OBJECT
39+ Q_PROPERTY(Workspace* activeWorkspace READ activeWorkspace WRITE setActiveWorkspace2 NOTIFY activeWorkspaceChanged)
40+
41+public:
42+ WorkspaceManager();
43+ static WorkspaceManager* instance();
44+
45+ Workspace* activeWorkspace() const;
46+ void setActiveWorkspace(Workspace* workspace);
47+
48+ Workspace* createWorkspace();
49+ void destroyWorkspace(Workspace* workspace);
50+
51+ void destroyFloatingWorkspaces();
52+
53+ Q_INVOKABLE void moveSurfaceToWorkspace(unity::shell::application::MirSurfaceInterface* surface,
54+ Workspace* workspace);
55+
56+ Q_INVOKABLE void moveWorkspaceContentToWorkspace(Workspace* to, Workspace* from);
57+
58+Q_SIGNALS:
59+ void activeWorkspaceChanged(Workspace*);
60+
61+private:
62+ void setActiveWorkspace2(Workspace* workspace);
63+
64+ QSet<Workspace*> m_allWorkspaces;
65+ Workspace* m_activeWorkspace;
66+ unity::shell::application::SurfaceManagerInterface* m_surfaceManager;
67+};
68+
69+#endif // WORKSPACEMANAGER_H
plugins/WindowManager/WorkspaceModel.cppnew file+254
@@ -0,0 +1,254 @@
1+/*
2+ * Copyright (C) 2017 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+#include "WorkspaceModel.h"
18+#include "WorkspaceManager.h"
19+#include "Workspace.h"
20+#include "Screen.h"
21+
22+#include <QQmlEngine>
23+
24+Q_LOGGING_CATEGORY(WORKSPACES, "Workspaces", QtInfoMsg)
25+
26+#define DEBUG_MSG qCDebug(WORKSPACES).nospace().noquote() << __func__
27+#define INFO_MSG qCInfo(WORKSPACES).nospace().noquote() << __func__
28+
29+WorkspaceModel::WorkspaceModel(QObject *parent)
30+ : QAbstractListModel(parent)
31+{
32+}
33+
34+WorkspaceModel::~WorkspaceModel()
35+{
36+ qDeleteAll(m_workspaces.toList()); // make a copy so the list doesnt edit itself during delete.
37+ m_workspaces.clear();
38+}
39+
40+void WorkspaceModel::append(Workspace *workspace)
41+{
42+ insert(m_workspaces.count(), workspace);
43+}
44+
45+void WorkspaceModel::insert(int index, Workspace *workspace)
46+{
47+ beginInsertRows(QModelIndex(), index, index);
48+
49+ m_workspaces.insert(index, workspace);
50+
51+ endInsertRows();
52+
53+ Q_EMIT workspaceInserted(index, workspace);
54+ Q_EMIT countChanged();
55+}
56+
57+void WorkspaceModel::remove(Workspace *workspace)
58+{
59+ int index = m_workspaces.indexOf(workspace);
60+ if (index < 0) return;
61+
62+ beginRemoveRows(QModelIndex(), index, index);
63+
64+ m_workspaces.removeAt(index);
65+ insertUnassigned(workspace);
66+
67+ endRemoveRows();
68+
69+ Q_EMIT workspaceRemoved(workspace);
70+ Q_EMIT countChanged();
71+}
72+
73+void WorkspaceModel::move(int from, int to)
74+{
75+ if (from == to) return;
76+ DEBUG_MSG << " from=" << from << " to=" << to;
77+
78+ if (from >= 0 && from < m_workspaces.size() && to >= 0 && to < m_workspaces.size()) {
79+ QModelIndex parent;
80+
81+ beginMoveRows(parent, from, from, parent, to + (to > from ? 1 : 0));
82+#if QT_VERSION < QT_VERSION_CHECK(5, 6, 0)
83+ const auto &window = m_windowModel.takeAt(from);
84+ m_workspaces.insert(to, window);
85+#else
86+ m_workspaces.move(from, to);
87+#endif
88+ endMoveRows();
89+
90+ Q_EMIT workspaceMoved(from, to);
91+ }
92+}
93+
94+int WorkspaceModel::indexOf(Workspace *workspace) const
95+{
96+ return m_workspaces.indexOf(workspace);
97+}
98+
99+Workspace *WorkspaceModel::get(int index) const
100+{
101+ if (index < 0 || index >= rowCount()) return nullptr;
102+ return m_workspaces.at(index);
103+}
104+
105+int WorkspaceModel::rowCount(const QModelIndex &) const
106+{
107+ return m_workspaces.count();
108+}
109+
110+QVariant WorkspaceModel::data(const QModelIndex &index, int role) const
111+{
112+ if (index.row() < 0 || index.row() >= m_workspaces.size())
113+ return QVariant();
114+
115+ if (role == WorkspaceRole) {
116+ Workspace *workspace = m_workspaces.at(index.row());
117+ return QVariant::fromValue(workspace);
118+ } else {
119+ return QVariant();
120+ }
121+}
122+
123+void WorkspaceModel::sync(WorkspaceModel *proxy)
124+{
125+ if (!proxy) return;
126+ const auto& proxyList = proxy->list();
127+
128+ // check for removals
129+ int removedIndexWhichWasActive = -1;
130+ QVector<Workspace*> dpCpy(this->list());
131+ Q_FOREACH(auto workspace, dpCpy) {
132+
133+ bool found = false;
134+ Q_FOREACH(auto p, proxyList) {
135+ auto workspaceProxy = qobject_cast<ProxyWorkspace*>(p);
136+ if (workspaceProxy->proxyObject() == workspace) {
137+ found = true;
138+ break;
139+ }
140+ }
141+ if (!found) {
142+ if (workspace->isActive()) {
143+ removedIndexWhichWasActive = indexOf(workspace);
144+ }
145+ workspace->unassign();
146+ }
147+ }
148+
149+ // existing
150+ QSet<Workspace*> newWorkspaces;
151+ for (int i = 0; i < proxyList.count(); i++) {
152+ auto workspaceProxy = qobject_cast<ProxyWorkspace*>(proxyList[i]);
153+ auto workspace = workspaceProxy->proxyObject();
154+
155+ int oldIndex = this->indexOf(workspace);
156+
157+ if (oldIndex < 0) {
158+ workspace->assign(this, QVariant(i));
159+ } else if (oldIndex != i) {
160+ this->move(oldIndex, i);
161+ }
162+ newWorkspaces.insert(workspace);
163+ }
164+
165+ // Make sure we have at least one workspace in the model.
166+ if (rowCount() == 0) {
167+ Workspace* workspace = WorkspaceManager::instance()->createWorkspace();
168+ workspace->assign(this);
169+ (new ProxyWorkspace(workspace))->assign(proxy);
170+ }
171+
172+ if (removedIndexWhichWasActive != -1) {
173+ int newActiveIndex = qMin(removedIndexWhichWasActive, this->rowCount()-1);
174+ Workspace* newActiveWorkspace = newActiveIndex >= 0 ? this->get(newActiveIndex) : nullptr;
175+
176+ WorkspaceManager::instance()->setActiveWorkspace(newActiveWorkspace);
177+ }
178+
179+ proxy->finishSync();
180+ finishSync();
181+}
182+
183+void WorkspaceModel::finishSync()
184+{
185+ QSet<Workspace*> dpCpy(m_unassignedWorkspaces);
186+ Q_FOREACH(auto workspace, dpCpy) {
187+ delete workspace;
188+ }
189+ m_unassignedWorkspaces.clear();
190+}
191+
192+void WorkspaceModel::insertUnassigned(Workspace *workspace)
193+{
194+ m_unassignedWorkspaces.insert(workspace);
195+ connect(workspace, &Workspace::assigned, this, [=]() {
196+ m_unassignedWorkspaces.remove(workspace);
197+ disconnect(workspace, &Workspace::assigned, this, 0);
198+ });
199+ connect(workspace, &QObject::destroyed, this, [=]() {
200+ m_unassignedWorkspaces.remove(workspace);
201+ });
202+}
203+
204+
205+ProxyWorkspaceModel::ProxyWorkspaceModel(WorkspaceModel * const model, ProxyScreen* screen)
206+ : m_original(model)
207+ , m_screen(screen)
208+{
209+ Q_FOREACH(auto workspace, model->list()) {
210+ auto proxy = new ProxyWorkspace(workspace);
211+ QQmlEngine::setObjectOwnership(proxy, QQmlEngine::CppOwnership);
212+ proxy->assign(this);
213+ }
214+ connect(m_original, &WorkspaceModel::workspaceInserted, this, [this](int index, Workspace* inserted) {
215+ if (isSyncing()) return;
216+
217+ (new ProxyWorkspace(inserted))->assign(this, index);
218+ });
219+ connect(m_original, &WorkspaceModel::workspaceRemoved, this, [this](Workspace* removed) {
220+ if (isSyncing()) return;
221+
222+ for (int i = 0; i < rowCount(); i++) {
223+ auto workspaceProxy = qobject_cast<ProxyWorkspace*>(get(i));
224+ auto w = workspaceProxy->proxyObject();
225+ if (w == removed) {
226+ remove(workspaceProxy);
227+ break;
228+ }
229+ }
230+ });
231+ connect(m_original, &WorkspaceModel::workspaceMoved, this, [this](int from, int to) {
232+ if (isSyncing()) return;
233+
234+ move(from, to);
235+ });
236+}
237+
238+void ProxyWorkspaceModel::move(int from, int to)
239+{
240+ WorkspaceModel::move(from, to);
241+}
242+
243+bool ProxyWorkspaceModel::isSyncing() const
244+{
245+ return m_screen->isSyncing();
246+}
247+
248+void ProxyWorkspaceModel::addWorkspace()
249+{
250+ auto newWorkspace = WorkspaceManager::instance()->createWorkspace();
251+ m_original->insertUnassigned(newWorkspace);
252+
253+ (new ProxyWorkspace(newWorkspace))->assign(this);
254+}
plugins/WindowManager/WorkspaceModel.hnew file+102
@@ -0,0 +1,102 @@
1+/*
2+ * Copyright (C) 2017 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+#ifndef WORKSPACEMODEL_H
18+#define WORKSPACEMODEL_H
19+
20+#include <QAbstractListModel>
21+#include <QLoggingCategory>
22+#include <QPointer>
23+
24+Q_DECLARE_LOGGING_CATEGORY(WORKSPACES)
25+
26+class Workspace;
27+class ProxyWorkspaceModel;
28+class ProxyScreen;
29+
30+class WorkspaceModel : public QAbstractListModel
31+{
32+ Q_OBJECT
33+ Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
34+public:
35+ /**
36+ * @brief The Roles supported by the model
37+ *
38+ * WorkspaceRole - A workspace.
39+ */
40+ enum Roles {
41+ WorkspaceRole = Qt::UserRole
42+ };
43+
44+ explicit WorkspaceModel(QObject *parent = 0);
45+ ~WorkspaceModel();
46+
47+ void append(Workspace *workspace);
48+ void insert(int index, Workspace *workspace);
49+ void remove(Workspace* workspace);
50+ virtual void move(int from, int to);
51+
52+ Q_INVOKABLE int indexOf(Workspace *workspace) const;
53+ Q_INVOKABLE Workspace* get(int index) const;
54+
55+ // From QAbstractItemModel
56+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
57+ QVariant data(const QModelIndex& index, int role) const override;
58+ QHash<int, QByteArray> roleNames() const override {
59+ QHash<int, QByteArray> roleNames { {WorkspaceRole, "workspace"} };
60+ return roleNames;
61+ }
62+
63+ const QVector<Workspace*>& list() const { return m_workspaces; }
64+
65+ void sync(WorkspaceModel* proxy);
66+ void finishSync();
67+
68+Q_SIGNALS:
69+ void countChanged();
70+
71+ void workspaceInserted(int index, Workspace *workspace);
72+ void workspaceRemoved(Workspace *workspace);
73+ void workspaceMoved(int from, int to);
74+
75+protected:
76+ void insertUnassigned(Workspace* workspace);
77+
78+ QVector<Workspace*> m_workspaces;
79+ QSet<Workspace*> m_unassignedWorkspaces;
80+
81+ friend class ProxyWorkspaceModel;
82+};
83+
84+class ProxyWorkspaceModel : public WorkspaceModel
85+{
86+ Q_OBJECT
87+public:
88+ explicit ProxyWorkspaceModel(WorkspaceModel*const model, ProxyScreen* screen);
89+
90+ Q_INVOKABLE void move(int from, int to) override;
91+
92+ bool isSyncing() const;
93+
94+public Q_SLOTS:
95+ void addWorkspace();
96+
97+protected:
98+ const QPointer<WorkspaceModel> m_original;
99+ const ProxyScreen* m_screen;
100+};
101+
102+#endif // WORKSPACEMODEL_H
qml/ApplicationMenus/ApplicationMenuItemFactory.qml+4−4
@@ -45,7 +45,7 @@ Object {
4545
4646 Action {
4747 id: action
48− text: menuData.label.replace("_", "&")
48+ text: menuData && menuData.label.replace("_", "&") || ""
4949 }
5050
5151 ListItemLayout {
@@ -59,8 +59,8 @@ Object {
5959 }
6060
6161 Label {
62− text: menuData.shortcut
63− visible: menuData.shortcut && QuickUtils.keyboardAttached
62+ text: menuData && menuData.shortcut || ""
63+ visible: menuData && menuData.shortcut && QuickUtils.keyboardAttached
6464 SlotsLayout.position: SlotsLayout.Trailing
6565 color: enabled ? theme.palette.normal.backgroundSecondaryText :
6666 theme.palette.disabled.backgroundSecondaryText
@@ -86,7 +86,7 @@ Object {
8686
8787 Action {
8888 id: action
89− text: menuData.label.replace("_", "&")
89+ text: menuData && menuData.label.replace("_", "&") || ""
9090 }
9191
9292 ListItemLayout {
qml/ApplicationMenus/MenuBar.qml+5−1
@@ -19,6 +19,7 @@ import QtQuick.Layouts 1.1
1919import Utils 0.1
2020import Ubuntu.Components 1.3
2121import GlobalShortcut 1.0
22+import "../Components/PanelState"
2223
2324Item {
2425 id: root
@@ -29,6 +30,7 @@ Item {
2930 property bool enableKeyFilter: false
3031 property real overflowWidth: width
3132 property bool windowMoving: false
33+ property PanelState panelState
3234
3335 // read from outside
3436 readonly property bool valid: rowRepeater.count > 0
@@ -106,7 +108,9 @@ Item {
106108
107109 Component {
108110 id: menuComponent
109− MenuPopup { }
111+ MenuPopup {
112+ panelState: root.panelState
113+ }
110114 }
111115
112116 Repeater {
qml/ApplicationMenus/MenuPopup.qml+3−1
@@ -64,6 +64,7 @@ UbuntuShape {
6464 }
6565
6666 property alias unityMenuModel: repeater.model
67+ property PanelState panelState
6768
6869 function show() {
6970 visible = true;
@@ -103,7 +104,7 @@ UbuntuShape {
103104 property real __minimumWidth: units.gu(20)
104105 property real __maximumWidth: ApplicationMenusLimits.screenWidth * 0.7
105106 property real __minimumHeight: units.gu(2)
106− property real __maximumHeight: ApplicationMenusLimits.screenHeight - PanelState.panelHeight
107+ property real __maximumHeight: ApplicationMenusLimits.screenHeight - panelState.panelHeight
107108
108109 signal dismissAll()
109110
@@ -492,6 +493,7 @@ UbuntuShape {
492493
493494 onLoaded: {
494495 item.unityMenuModel = Qt.binding(function() { return submenuLoader.unityMenuModel; });
496+ item.panelState = Qt.binding(function() { return root.panelState; });
495497 item.objectName = Qt.binding(function() { return submenuLoader.objectName + "menu"; });
496498 item.desiredX = Qt.binding(function() { return submenuLoader.desiredX; });
497499 item.desiredY = Qt.binding(function() { return submenuLoader.desiredY; });
qml/CMakeLists.txt+1−1
@@ -1,4 +1,4 @@
1−file(GLOB QML_JS_FILES *.qml *.js)
1+file(GLOB QML_JS_FILES *.qml *.js qmldir)
22
33install(FILES ${QML_JS_FILES}
44 DESTINATION ${SHELL_APP_DIR}
qml/Components/PanelState/PanelState.qml−1
@@ -14,7 +14,6 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17−pragma Singleton
1817import QtQuick 2.4
1918
2019QtObject {
qml/Components/PanelState/qmldir+1−1
@@ -1 +1 @@
1−singleton PanelState 1.0 PanelState.qml
1+PanelState 1.0 PanelState.qml
qml/Components/VirtualTouchPad.qml+14−20
@@ -18,22 +18,20 @@ import QtQuick 2.4
1818import QtQuick.Layouts 1.1
1919import Ubuntu.Components 1.3
2020import Qt.labs.settings 1.0
21−import Unity.Screens 0.1
21+import WindowManager 1.0
2222import UInput 0.1
2323import "../Components"
2424
2525Item {
2626 id: root
27− property var uinput: UInput {
28− Component.onCompleted: createMouse();
29− Component.onDestruction: removeMouse();
30− }
3127
3228 Component.onCompleted: {
29+ UInput.createMouse();
3330 if (!settings.touchpadTutorialHasRun) {
3431 root.runTutorial()
3532 }
3633 }
34+ Component.onDestruction: UInput.removeMouse()
3735
3836 function runTutorial() {
3937 // If the tutorial animation is started too early, e.g. in Component.onCompleted,
@@ -85,7 +83,7 @@ Item {
8583 if (((point1.pressed && !point2.pressed) || (!point1.pressed && point2.pressed))
8684 && clickTimer.running) {
8785 clickTimer.stop();
88− uinput.pressMouse(UInput.ButtonLeft)
86+ UInput.pressMouse(UInput.ButtonLeft)
8987 isDoubleClick = true;
9088 }
9189 isClick = true;
@@ -104,7 +102,7 @@ Item {
104102
105103 onReleased: {
106104 if (isDoubleClick || isDrag) {
107− uinput.releaseMouse(UInput.ButtonLeft)
105+ UInput.releaseMouse(UInput.ButtonLeft)
108106 isDoubleClick = false;
109107 }
110108 if (isClick) {
@@ -120,8 +118,8 @@ Item {
120118 interval: 200
121119 property int button: UInput.ButtonLeft
122120 onTriggered: {
123− uinput.pressMouse(button);
124− uinput.releaseMouse(button);
121+ UInput.pressMouse(button);
122+ UInput.releaseMouse(button);
125123 }
126124 function scheduleClick(button) {
127125 clickTimer.button = button;
@@ -138,7 +136,7 @@ Item {
138136 isDrag = true;
139137 }
140138
141− uinput.moveMouse(tp.x - tp.previousX, tp.y - tp.previousY);
139+ UInput.moveMouse(tp.x - tp.previousX, tp.y - tp.previousY);
142140 }
143141
144142 function scroll(touchPoints) {
@@ -166,7 +164,7 @@ Item {
166164 dh /= 2;
167165 dv /= 2;
168166
169− uinput.scrollMouse(dh, dv);
167+ UInput.scrollMouse(dh, dv);
170168 }
171169
172170 touchPoints: [
@@ -189,8 +187,8 @@ Item {
189187 objectName: "leftButton"
190188 Layout.fillWidth: true
191189 Layout.fillHeight: true
192− onPressed: uinput.pressMouse(UInput.ButtonLeft);
193− onReleased: uinput.releaseMouse(UInput.ButtonLeft);
190+ onPressed: UInput.pressMouse(UInput.ButtonLeft);
191+ onReleased: UInput.releaseMouse(UInput.ButtonLeft);
194192 property bool highlight: false
195193 UbuntuShape {
196194 anchors.fill: parent
@@ -204,8 +202,8 @@ Item {
204202 objectName: "rightButton"
205203 Layout.fillWidth: true
206204 Layout.fillHeight: true
207− onPressed: uinput.pressMouse(UInput.ButtonRight);
208− onReleased: uinput.releaseMouse(UInput.ButtonRight);
205+ onPressed: UInput.pressMouse(UInput.ButtonRight);
206+ onReleased: UInput.releaseMouse(UInput.ButtonRight);
209207 property bool highlight: false
210208 UbuntuShape {
211209 anchors.fill: parent
@@ -239,14 +237,10 @@ Item {
239237 }
240238 }
241239
242− Screens {
243− id: screens
244− }
245−
246240 InputMethod {
247241 id: inputMethod
248242 // Don't resize when there is only one screen to avoid resize clashing with the InputMethod in the Shell.
249− enabled: screens.count > 1 && settings.oskEnabled && !tutorial.running
243+ enabled: Screens.count > 1 && settings.oskEnabled && !tutorial.running
250244 objectName: "inputMethod"
251245 anchors.fill: parent
252246 }
qml/DisabledScreenNotice.qml+2−1
@@ -28,8 +28,9 @@ Item {
2828 property var screen: Screen
2929 property var orientationLock: OrientationLock
3030
31+ property alias deviceConfiguration: _deviceConfiguration
3132 DeviceConfiguration {
32− id: deviceConfiguration
33+ id: _deviceConfiguration
3334 name: applicationArguments.deviceName
3435 }
3536
qml/ErrorApplication.qmlnew file+67
@@ -0,0 +1,67 @@
1+/*
2+* Copyright (C) 2017 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 QtQuick.Window 2.2
19+import Ubuntu.Components 1.3
20+import WindowManager 1.0
21+
22+Instantiator {
23+ id: root
24+ model: Screens
25+
26+ ScreenWindow {
27+ id: window
28+ objectName: "screen"+index
29+ screen: model.screen
30+ visibility: applicationArguments.hasFullscreen ? Window.FullScreen : Window.Windowed
31+ flags: applicationArguments.hasFrameless ? Qt.FramelessWindowHint : 0
32+
33+ Binding {
34+ when: applicationArguments.hasGeometry
35+ target: window
36+ property: "width"
37+ value: applicationArguments.windowGeometry.width
38+ }
39+ Binding {
40+ when: applicationArguments.hasGeometry
41+ target: window
42+ property: "height"
43+ value: applicationArguments.windowGeometry.height
44+ }
45+
46+ Loader {
47+ width: window.width
48+ height: window.height
49+
50+ Rectangle {
51+ color: "white"
52+ Column {
53+ spacing: units.gu(1)
54+
55+ Label {
56+ text: "Unity encountered an unrecoverable error while loading:"
57+ fontSize: "large"
58+ }
59+
60+ Label {
61+ text: errorString
62+ }
63+ }
64+ }
65+ }
66+ }
67+}
qml/Greeter/Greeter.qml+21−3
@@ -24,6 +24,7 @@ import Unity.Launcher 0.1
2424import Unity.Session 0.1
2525
2626import "." 0.1
27+import ".." 0.1
2728import "../Components"
2829
2930Showable {
@@ -225,7 +226,7 @@ Showable {
225226 if (forcedUnlock && shown) {
226227 hideView();
227228 if (hideNow) {
228− root.hideNow(); // skip hide animation
229+ ShellNotifier.greeter.hide(true); // skip hide animation
229230 }
230231 }
231232 }
@@ -373,7 +374,7 @@ Showable {
373374 onEmergencyCall: root.emergencyCall()
374375 onRequiredChanged: {
375376 if (!loader.item.required) {
376− root.hide();
377+ ShellNotifier.greeter.hide(false);
377378 }
378379 }
379380 }
@@ -487,12 +488,29 @@ Showable {
487488 onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user))
488489 }
489490
491+ Connections {
492+ target: ShellNotifier.greeter
493+ onHide: {
494+ if (now) {
495+ root.hideNow(); // skip hide animation
496+ } else {
497+ root.hide();
498+ }
499+ }
500+ }
501+
502+ Binding {
503+ target: ShellNotifier.greeter
504+ property: "shown"
505+ value: root.shown
506+ }
507+
490508 Connections {
491509 target: DBusUnitySessionService
492510 onLockRequested: root.forceShow()
493511 onUnlocked: {
494512 root.forcedUnlock = true;
495− root.hideNow();
513+ ShellNotifier.greeter.hide(true);
496514 }
497515 }
498516
qml/Greeter/SecondaryGreeter.qmlnew file+75
@@ -0,0 +1,75 @@
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+
20+import "../Components"
21+import ".." 0.1
22+
23+Showable {
24+ id: root
25+
26+ readonly property bool active: required || hasLockedApp
27+
28+ readonly property bool hasLockedApp: lockedApp !== ""
29+ readonly property bool locked: false
30+ readonly property bool waiting: false
31+ readonly property bool fullyShown: shown
32+
33+ property string lockedApp: ""
34+
35+ function forceShow() { show(); }
36+ property var notifyAppFocusRequested: (function(appId) { return; })
37+ property var notifyUserRequestedApp: (function(appId) { return; })
38+ property var notifyShowingDashFromDrag: (function(appId) { return false; })
39+
40+ showAnimation: StandardAnimation { property: "opacity"; to: 1 }
41+ hideAnimation: StandardAnimation { property: "opacity"; to: 0 }
42+
43+ shown: ShellNotifier.greeter.shown
44+ Component.onCompleted: opacity = shown ? 1 : 0
45+ visible: opacity != 0
46+
47+ Rectangle {
48+ anchors.fill: parent
49+ color: UbuntuColors.purple
50+ }
51+
52+ MouseArea {
53+ anchors.fill: parent
54+ acceptedButtons: Qt.AllButtons
55+ onWheel: wheel.accepted = true
56+ }
57+
58+ Connections {
59+ target: ShellNotifier.greeter
60+ onHide: {
61+ if (now) {
62+ root.hideNow(); // skip hide animation
63+ } else {
64+ root.hide();
65+ }
66+ }
67+ onShownChanged: {
68+ if (ShellNotifier.greeter.shown) {
69+ root.show();
70+ } else {
71+ root.hide();
72+ }
73+ }
74+ }
75+}
qml/OrientedShell.qml+5−8
@@ -18,7 +18,7 @@ import QtQuick 2.4
1818import QtQuick.Window 2.2
1919import Unity.InputInfo 0.1
2020import Unity.Session 0.1
21−import Unity.Screens 0.1
21+import WindowManager 1.0
2222import Utils 0.1
2323import GSettings 1.0
2424import "Components"
@@ -32,15 +32,16 @@ Item {
3232 implicitWidth: units.gu(40)
3333 implicitHeight: units.gu(71)
3434
35+ property alias deviceConfiguration: _deviceConfiguration
36+ property alias orientations: d.orientations
37+
3538 onWidthChanged: calculateUsageMode();
3639
3740 DeviceConfiguration {
38− id: deviceConfiguration
41+ id: _deviceConfiguration
3942 name: applicationArguments.deviceName
4043 }
4144
42− property alias orientations: d.orientations
43−
4445 Item {
4546 id: d
4647
@@ -157,10 +158,6 @@ Item {
157158 return false;
158159 }
159160
160− Screens {
161− id: screens
162− }
163−
164161 property int orientation
165162 onPhysicalOrientationChanged: {
166163 if (!orientationLocked) {
qml/Panel/Panel.qml+18−16
@@ -48,6 +48,7 @@ Item {
4848 property bool hasKeyboard: false
4949
5050 property string mode: "staged"
51+ property PanelState panelState
5152
5253 MouseArea {
5354 id: backMouseEater
@@ -64,14 +65,14 @@ Item {
6465 }
6566
6667 Binding {
67− target: PanelState
68+ target: panelState
6869 property: "panelHeight"
6970 value: minimizedPanelHeight
7071 }
7172
7273 RegisteredApplicationMenuModel {
7374 id: registeredMenuModel
74− persistentSurfaceId: PanelState.focusedPersistentSurfaceId
75+ persistentSurfaceId: panelState.focusedPersistentSurfaceId
7576 }
7677
7778 QtObject {
@@ -82,11 +83,11 @@ Item {
8283 !indicators.shown &&
8384 (decorationMouseArea.containsMouse || menuBarLoader.menusRequested)
8485
85− property bool showWindowDecorationControls: (revealControls && PanelState.decorationsVisible) ||
86− PanelState.decorationsAlwaysVisible
86+ property bool showWindowDecorationControls: (revealControls && panelState.decorationsVisible) ||
87+ panelState.decorationsAlwaysVisible
8788
88− property bool showPointerMenu: revealControls && enablePointerMenu &&
89− (PanelState.decorationsVisible || mode == "windowed")
89+ property bool showPointerMenu: revealControls &&
90+ (panelState.decorationsVisible || mode == "windowed")
9091
9192 property bool enablePointerMenu: applicationMenus.available &&
9293 applicationMenus.model
@@ -138,7 +139,7 @@ Item {
138139 fill: panelAreaBackground
139140 bottomMargin: -units.gu(1)
140141 }
141− visible: PanelState.dropShadow
142+ visible: panelState.dropShadow
142143 source: "graphics/rectangular_dropshadow.sci"
143144 }
144145
@@ -195,12 +196,12 @@ Item {
195196 visible: opacity != 0
196197 Behavior on opacity { UbuntuNumberAnimation { duration: UbuntuAnimation.SnapDuration } }
197198
198− active: PanelState.decorationsVisible || PanelState.decorationsAlwaysVisible
199+ active: panelState.decorationsVisible || panelState.decorationsAlwaysVisible
199200 windowIsMaximized: true
200− onCloseClicked: PanelState.closeClicked()
201− onMinimizeClicked: PanelState.minimizeClicked()
202− onMaximizeClicked: PanelState.restoreClicked()
203− closeButtonShown: PanelState.closeButtonShown
201+ onCloseClicked: panelState.closeClicked()
202+ onMinimizeClicked: panelState.minimizeClicked()
203+ onMaximizeClicked: panelState.restoreClicked()
204+ closeButtonShown: panelState.closeButtonShown
204205 }
205206
206207 Loader {
@@ -220,11 +221,12 @@ Item {
220221 sourceComponent: MenuBar {
221222 id: bar
222223 objectName: "menuBar"
223− anchors.left: parent ? parent.left : undefined
224+ anchors.left: menuBarLoader ? menuBarLoader.left : undefined
224225 anchors.margins: units.gu(1)
225226 height: menuBarLoader.height
226− enableKeyFilter: valid && PanelState.decorationsVisible
227+ enableKeyFilter: valid && panelState.decorationsVisible
227228 unityMenuModel: __applicationMenus.model
229+ panelState: root.panelState
228230
229231 Connections {
230232 target: __applicationMenus
@@ -236,7 +238,7 @@ Item {
236238 onShownChanged: bar.dismiss();
237239 }
238240
239− onDoubleClicked: PanelState.restoreClicked()
241+ onDoubleClicked: panelState.restoreClicked()
240242 onPressed: mouse.accepted = false // let the parent mouse area handle this, so it can both unsnap window and show menu
241243 }
242244 }
@@ -343,7 +345,7 @@ Item {
343345 opacity: __applicationMenus.visible && !__applicationMenus.expanded ? 1 : 0
344346 visible: opacity != 0
345347 Behavior on opacity { NumberAnimation { duration: UbuntuAnimation.SnapDuration } }
346− text: PanelState.title
348+ text: panelState.title
347349 }
348350
349351 PanelMenu {
qml/Shell.qml+41−25
@@ -1,4 +1,4 @@
1−/*
1+/*
22 * Copyright (C) 2013-2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
@@ -38,6 +38,7 @@ import "Notifications"
3838import "Stage"
3939import "Tutorial"
4040import "Wizard"
41+import "Components/PanelState"
4142import Unity.Notifications 1.0 as NotificationBackend
4243import Unity.Session 0.1
4344import Unity.Indicators 0.1 as Indicators
@@ -95,6 +96,11 @@ StyledItem {
9596
9697 readonly property var mainApp: stage.mainApp
9798
99+ readonly property var topLevelSurfaceList: {
100+ if (!WMScreen.currentWorkspace) return null;
101+ return stage.temporarySelectedWorkspace ? stage.temporarySelectedWorkspace.windowModel : WMScreen.currentWorkspace.windowModel
102+ }
103+
98104 onMainAppChanged: {
99105 if (mainApp) {
100106 _onMainAppChanged(mainApp.appId);
@@ -183,11 +189,11 @@ StyledItem {
183189 }
184190
185191 function startApp(appId) {
186− if (ApplicationManager.findApplication(appId)) {
187− ApplicationManager.requestFocusApplication(appId);
188− } else {
192+ if (!ApplicationManager.findApplication(appId)) {
189193 ApplicationManager.startApplication(appId);
190194 }
195+ ApplicationManager.requestFocusApplication(appId);
196+ stage.closeSpread();
191197 }
192198
193199 function startLockedApp(app) {
@@ -261,23 +267,17 @@ StyledItem {
261267 schema.id: "com.canonical.Unity8"
262268 }
263269
270+ PanelState {
271+ id: panelState
272+ objectName: "panelState"
273+ }
274+
264275 Item {
265276 id: stages
266277 objectName: "stages"
267278 width: parent.width
268279 height: parent.height
269280
270− SurfaceManager {
271− id: surfaceMan
272− objectName: "surfaceManager"
273− }
274− TopLevelWindowModel {
275− id: topLevelSurfaceList
276− objectName: "topLevelSurfaceList"
277− applicationManager: ApplicationManager // it's a singleton
278− surfaceManager: surfaceMan
279− }
280−
281281 Stage {
282282 id: stage
283283 objectName: "stage"
@@ -288,7 +288,7 @@ StyledItem {
288288 background: wallpaperResolver.background
289289
290290 applicationManager: ApplicationManager
291− topLevelSurfaceList: topLevelSurfaceList
291+ topLevelSurfaceList: shell.topLevelSurfaceList
292292 inputMethodRect: inputMethod.visibleRect
293293 rightEdgePushProgress: rightEdgeBarrier.progress
294294 availableDesktopArea: availableDesktopAreaItem
@@ -316,6 +316,7 @@ StyledItem {
316316 altTabPressed: physicalKeysMapper.altTabPressed
317317 oskEnabled: shell.oskEnabled
318318 spreadEnabled: tutorial.spreadEnabled && (!greeter || (!greeter.hasLockedApp && !greeter.shown))
319+ panelState: panelState
319320
320321 onSpreadShownChanged: {
321322 panel.indicators.hide();
@@ -356,7 +357,7 @@ StyledItem {
356357 InputMethod {
357358 id: inputMethod
358359 objectName: "inputMethod"
359− surface: topLevelSurfaceList.inputMethodSurface
360+ surface: shell.topLevelSurfaceList.inputMethodSurface
360361 anchors {
361362 fill: parent
362363 topMargin: panel.panelHeight
@@ -370,8 +371,13 @@ StyledItem {
370371 objectName: "greeterLoader"
371372 anchors.fill: parent
372373 anchors.topMargin: panel.panelHeight
373− sourceComponent: shell.mode != "shell" ? integratedGreeter :
374− Qt.createComponent(Qt.resolvedUrl("Greeter/ShimGreeter.qml"));
374+ sourceComponent: {
375+ if (shell.mode != "shell") {
376+ if (screenWindow.primary) return integratedGreeter;
377+ return secondaryGreeter;
378+ }
379+ return Qt.createComponent(Qt.resolvedUrl("Greeter/ShimGreeter.qml"));
380+ }
375381 onLoaded: {
376382 item.objectName = "greeter"
377383 }
@@ -441,6 +447,13 @@ StyledItem {
441447 }
442448 }
443449
450+ Component {
451+ id: secondaryGreeter
452+ SecondaryGreeter {
453+ hides: [launcher, panel.indicators]
454+ }
455+ }
456+
444457 Timer {
445458 // See powerConnection for why this is useful
446459 id: showGreeterDelayed
@@ -551,13 +564,14 @@ StyledItem {
551564 && !stage.spreadShown
552565 }
553566
554− readonly property bool focusedSurfaceIsFullscreen: topLevelSurfaceList.focusedWindow
555− ? topLevelSurfaceList.focusedWindow.state == Mir.FullscreenState
567+ readonly property bool focusedSurfaceIsFullscreen: shell.topLevelSurfaceList.focusedWindow
568+ ? shell.topLevelSurfaceList.focusedWindow.state == Mir.FullscreenState
556569 : false
557570 fullscreenMode: (focusedSurfaceIsFullscreen && !LightDMService.greeter.active && launcher.progress == 0 && !stage.spreadShown)
558571 || greeter.hasLockedApp
559572 greeterShown: greeter && greeter.shown
560573 hasKeyboard: shell.hasKeyboard
574+ panelState: panelState
561575 }
562576
563577 Launcher {
@@ -806,11 +820,11 @@ StyledItem {
806820 Cursor {
807821 id: cursor
808822 objectName: "cursor"
809− visible: shell.hasMouse
823+
810824 z: itemGrabber.z + 1
811825 topBoundaryOffset: panel.panelHeight
812−
813− confiningItem: stage.itemConfiningMouseCursor
826+ enabled: shell.hasMouse && screenWindow.active
827+ visible: enabled
814828
815829 property bool mouseNeverMoved: true
816830 Binding {
@@ -822,6 +836,8 @@ StyledItem {
822836 when: cursor.mouseNeverMoved && cursor.visible
823837 }
824838
839+ confiningItem: stage.itemConfiningMouseCursor
840+
825841 height: units.gu(3)
826842
827843 readonly property var previewRectangle: stage.previewRectangle.target &&
@@ -885,7 +901,7 @@ StyledItem {
885901
886902 // non-visual objects
887903 KeymapSwitcher {
888− focusedSurface: topLevelSurfaceList.focusedWindow ? topLevelSurfaceList.focusedWindow.surface : null
904+ focusedSurface: shell.topLevelSurfaceList.focusedWindow ? shell.topLevelSurfaceList.focusedWindow.surface : null
889905 }
890906 BrightnessControl {}
891907
qml/ShellApplication.qmlnew file+60
@@ -0,0 +1,60 @@
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 QtQuick.Window 2.2
19+import WindowManager 1.0
20+import Unity.Application 0.1
21+
22+Instantiator {
23+ id: root
24+ model: Screens
25+
26+ ShellScreen {
27+ id: window
28+ objectName: "screen"+index
29+ screen: model.screen
30+ visibility: applicationArguments.hasFullscreen ? Window.FullScreen : Window.Windowed
31+ flags: applicationArguments.hasFrameless ? Qt.FramelessWindowHint : 0
32+
33+ Binding {
34+ when: applicationArguments.hasGeometry
35+ target: window
36+ property: "width"
37+ value: applicationArguments.windowGeometry.width
38+ }
39+ Binding {
40+ when: applicationArguments.hasGeometry
41+ target: window
42+ property: "height"
43+ value: applicationArguments.windowGeometry.height
44+ }
45+
46+ Component.onCompleted: screen.active = primary
47+ primary: index == 0
48+ }
49+
50+ property var windowManagerSurfaceManagerBinding: Binding {
51+ target: WindowManagerObjects
52+ property: "surfaceManager"
53+ value: SurfaceManager
54+ }
55+ property var windowManagerApplicationManagerBinding: Binding {
56+ target: WindowManagerObjects
57+ property: "applicationManager"
58+ value: ApplicationManager
59+ }
60+}
qml/ShellNotifier.qmlnew file+30
@@ -0,0 +1,30 @@
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+/*
18+ Shared signals & properties on multi-window desktop
19+ */
20+
21+pragma Singleton
22+import QtQuick 2.4
23+
24+QtObject {
25+ property var greeter: QtObject {
26+ signal hide(bool now)
27+
28+ property bool shown: true
29+ }
30+}
qml/ShellScreen.qmlnew file+64
@@ -0,0 +1,64 @@
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 WindowManager 1.0
20+import Cursor 1.1
21+import "Components"
22+
23+ScreenWindow {
24+ id: screenWindow
25+
26+ color: "black"
27+ title: "Unity8 Shell"
28+ property bool primary: false
29+
30+ DeviceConfiguration {
31+ id: deviceConfiguration
32+ name: applicationArguments.deviceName
33+ }
34+
35+ Loader {
36+ id: loader
37+ width: screenWindow.width
38+ height: screenWindow.height
39+
40+ sourceComponent: {
41+ if (Screens.count > 1 && primary && deviceConfiguration.category !== "desktop") {
42+ return disabledScreenComponent;
43+ }
44+ return shellComponent;
45+ }
46+ }
47+
48+ Component {
49+ id: shellComponent
50+ OrientedShell {
51+ implicitWidth: screenWindow.width
52+ implicitHeight: screenWindow.height
53+
54+ deviceConfiguration {
55+ name: Screens.count > 1 ? "desktop" : applicationArguments.deviceName
56+ }
57+ }
58+ }
59+
60+ Component {
61+ id: disabledScreenComponent
62+ DisabledScreenNotice {}
63+ }
64+}
qml/Stage/ApplicationWindow.qml+10−207
@@ -52,13 +52,8 @@ FocusScope {
5252 // other instructions.
5353 if (surface) {
5454 surfaceContainer.surface = surface;
55− d.liveSurface = surface.live;
56− d.hadSurface = false;
5755 surfaceInitTimer.start();
5856 } else {
59− if (d.surfaceInitialized) {
60− d.hadSurface = true;
61− }
6257 d.surfaceInitialized = false;
6358 surfaceContainer.surface = null;
6459 }
@@ -67,21 +62,6 @@ FocusScope {
6762 QtObject {
6863 id: d
6964
70− property bool liveSurface: false;
71− property var con: Connections {
72− target: root.surface
73− onLiveChanged: d.liveSurface = root.surface.live
74− }
75− // using liveSurface instead of root.surface.live because with the latter
76− // this expression is not reevaluated when root.surface changes
77− readonly property bool needToTakeScreenshot: root.surface && d.surfaceInitialized && !d.liveSurface
78− && applicationState !== ApplicationInfoInterface.Running
79− onNeedToTakeScreenshotChanged: {
80− if (needToTakeScreenshot && screenshotImage.status === Image.Null) {
81− screenshotImage.take();
82− }
83− }
84−
8565 // helpers so that we don't have to check for the existence of an application everywhere
8666 // (in order to avoid breaking qml binding due to a javascript exception)
8767 readonly property string name: root.application ? root.application.name : ""
@@ -131,34 +111,10 @@ FocusScope {
131111 id: surfaceInitTimer
132112 interval: 100
133113 onTriggered: {
134− if (root.surface && root.surface.live) {d.surfaceInitialized = true;}
135− }
136− }
137−
138− Timer {
139− id: surfaceIsOldTimer
140− interval: 1000
141− onTriggered: { if (stateGroup.state === "surface") { d.surfaceOldEnoughToBeResized = true; } }
142− }
143−
144− Image {
145− id: screenshotImage
146− objectName: "screenshotImage"
147− anchors.fill: parent
148− fillMode: Image.PreserveAspectCrop
149− horizontalAlignment: Image.AlignLeft
150− verticalAlignment: Image.AlignTop
151− antialiasing: !root.interactive
152− z: 1
153−
154− function take() {
155− // Save memory by using a half-resolution (thus quarter size) screenshot.
156− // Do not make this a binding, we can only take the screenshot once!
157− surfaceContainer.grabToImage(
158− function(result) {
159− screenshotImage.source = result.url;
160− },
161− Qt.size(root.width / 2, root.height / 2));
114+ if (root.surface && root.surface.live) {
115+ d.surfaceInitialized = true;
116+ d.hadSurface = true;
117+ }
162118 }
163119 }
164120
@@ -167,7 +123,7 @@ FocusScope {
167123 visible: active
168124 active: false
169125 anchors.fill: parent
170− z: screenshotImage.z + 1
126+ z: 1
171127 sourceComponent: Component {
172128 Splash {
173129 id: splash
@@ -238,167 +194,14 @@ FocusScope {
238194 id: stateGroup
239195 objectName: "applicationWindowStateGroup"
240196 states: [
241− State {
242− name: "void"
243− when:
244− d.hadSurface && (!root.surface || !d.surfaceInitialized)
245− &&
246− screenshotImage.status !== Image.Ready
247− },
248− State {
249− name: "splashScreen"
250− when:
251− !d.hadSurface && (!root.surface || !d.surfaceInitialized)
252− &&
253− screenshotImage.status !== Image.Ready
254− },
255− State {
197+ State{
256198 name: "surface"
257− when:
258− (root.surface && d.surfaceInitialized)
259− &&
260− (d.liveSurface ||
261− (d.applicationState !== ApplicationInfoInterface.Running
262− && screenshotImage.status !== Image.Ready))
263− PropertyChanges {
264− target: root
265− implicitWidth: surfaceContainer.implicitWidth
266− implicitHeight: surfaceContainer.implicitHeight
267− }
268− },
269− State {
270− name: "screenshot"
271− when:
272− screenshotImage.status === Image.Ready
273− &&
274− (d.applicationState !== ApplicationInfoInterface.Running
275− || !root.surface || !d.surfaceInitialized)
199+ when: (root.surface && d.surfaceInitialized) || d.hadSurface
276200 },
277201 State {
278− // This is a dead end. From here we expect the surface to be removed from the model
279− // shortly after we stop referencing to it in our SurfaceContainer.
280− name: "closed"
281− when:
282− // The surface died while the application is running. It must have been closed
283− // by the shell or the application decided to destroy it by itself
284− root.surface && d.surfaceInitialized && !d.liveSurface
285− && d.applicationState === ApplicationInfoInterface.Running
286− }
287− ]
288−
289− transitions: [
290− Transition {
291− from: ""; to: "splashScreen"
292− PropertyAction { target: splashLoader; property: "active"; value: true }
293− PropertyAction { target: surfaceContainer
294− property: "visible"; value: false }
295− },
296− Transition {
297− from: "splashScreen"; to: "surface"
298− SequentialAnimation {
299− PropertyAction { target: surfaceContainer
300− property: "opacity"; value: 0.0 }
301− PropertyAction { target: surfaceContainer
302− property: "visible"; value: true }
303− UbuntuNumberAnimation { target: surfaceContainer; property: "opacity";
304− from: 0.0; to: 1.0
305− duration: UbuntuAnimation.BriskDuration }
306− ScriptAction { script: {
307− splashLoader.active = false;
308− surfaceIsOldTimer.start();
309− } }
310− }
311− },
312− Transition {
313− from: "surface"; to: "splashScreen"
314− SequentialAnimation {
315− ScriptAction { script: {
316− surfaceIsOldTimer.stop();
317− d.surfaceOldEnoughToBeResized = false;
318− splashLoader.active = true;
319− surfaceContainer.visible = true;
320− } }
321− UbuntuNumberAnimation { target: splashLoader; property: "opacity";
322− from: 0.0; to: 1.0
323− duration: UbuntuAnimation.BriskDuration }
324− PropertyAction { target: surfaceContainer
325− property: "visible"; value: false }
326− }
327− },
328− Transition {
329− from: "surface"; to: "screenshot"
330− SequentialAnimation {
331− ScriptAction { script: {
332− surfaceIsOldTimer.stop();
333− d.surfaceOldEnoughToBeResized = false;
334− screenshotImage.visible = true;
335− } }
336− UbuntuNumberAnimation { target: screenshotImage; property: "opacity";
337− from: 0.0; to: 1.0
338− duration: UbuntuAnimation.BriskDuration }
339− ScriptAction { script: {
340− surfaceContainer.visible = false;
341− surfaceContainer.surface = null;
342− d.hadSurface = true;
343− } }
344− }
345− },
346− Transition {
347− from: "screenshot"; to: "surface"
348− SequentialAnimation {
349− PropertyAction { target: surfaceContainer
350− property: "visible"; value: true }
351− UbuntuNumberAnimation { target: screenshotImage; property: "opacity";
352− from: 1.0; to: 0.0
353− duration: UbuntuAnimation.BriskDuration }
354− ScriptAction { script: {
355− screenshotImage.visible = false;
356− screenshotImage.source = "";
357− surfaceIsOldTimer.start();
358− } }
359− }
360− },
361− Transition {
362− from: "splashScreen"; to: "screenshot"
363− SequentialAnimation {
364− PropertyAction { target: screenshotImage
365− property: "visible"; value: true }
366− UbuntuNumberAnimation { target: screenshotImage; property: "opacity";
367− from: 0.0; to: 1.0
368− duration: UbuntuAnimation.BriskDuration }
369− PropertyAction { target: splashLoader; property: "active"; value: false }
370− }
371− },
372− Transition {
373− from: "surface"; to: "void"
374− ScriptAction { script: {
375− surfaceIsOldTimer.stop();
376− d.surfaceOldEnoughToBeResized = false;
377− surfaceContainer.visible = false;
378− } }
379− },
380− Transition {
381− from: "void"; to: "surface"
382− SequentialAnimation {
383− PropertyAction { target: surfaceContainer; property: "opacity"; value: 0.0 }
384− PropertyAction { target: surfaceContainer; property: "visible"; value: true }
385− UbuntuNumberAnimation { target: surfaceContainer; property: "opacity";
386− from: 0.0; to: 1.0
387− duration: UbuntuAnimation.BriskDuration }
388− ScriptAction { script: {
389− surfaceIsOldTimer.start();
390− } }
391− }
392− },
393− Transition {
394− to: "closed"
395− SequentialAnimation {
396− ScriptAction { script: {
397− surfaceContainer.visible = false;
398− surfaceContainer.surface = null;
399− d.hadSurface = true;
400− } }
401− }
202+ name: "splash"
203+ when: !root.surface && !d.surfaceInitialized && !d.hadSurface
204+ PropertyChanges { target: splashLoader; active: true }
402205 }
403206 ]
404207 }
qml/Stage/DecoratedWindow.qml+13−11
@@ -39,6 +39,7 @@ FocusScope {
3939 property alias interactive: applicationWindow.interactive
4040 readonly property alias orientationChangesEnabled: applicationWindow.orientationChangesEnabled
4141 property alias windowControlButtonsVisible: decoration.windowControlButtonsVisible
42+ property PanelState panelState
4243
4344 // Changing this will actually add/remove a decoration, meaning, requestedHeight will take the decoration into account.
4445 property bool hasDecoration: true
@@ -126,15 +127,15 @@ FocusScope {
126127 name: "preview"; when: root.scaleToPreviewProgress > 0
127128 PropertyChanges {
128129 target: root
129− implicitWidth: MathUtils.linearAnimation(0, 1, applicationWindow.oldRequestedWidth, root.scaleToPreviewSize, root.scaleToPreviewProgress)
130− implicitHeight: MathUtils.linearAnimation(0, 1, applicationWindow.oldRequestedHeight, root.scaleToPreviewSize, root.scaleToPreviewProgress)
130+ implicitWidth: MathUtils.linearAnimation(0, 1, applicationWindow.requestedWidth, root.scaleToPreviewSize, root.scaleToPreviewProgress)
131+ implicitHeight: MathUtils.linearAnimation(0, 1, applicationWindow.requestedHeight, root.scaleToPreviewSize, root.scaleToPreviewProgress)
131132 }
132133 PropertyChanges {
133134 target: applicationWindow;
134− requestedWidth: applicationWindow.oldRequestedWidth
135− requestedHeight: applicationWindow.oldRequestedHeight
136− width: MathUtils.linearAnimation(0, 1, applicationWindow.oldRequestedWidth, applicationWindow.minSize, root.scaleToPreviewProgress)
137− height: MathUtils.linearAnimation(0, 1, applicationWindow.oldRequestedHeight, applicationWindow.minSize, root.scaleToPreviewProgress)
135+// requestedWidth: applicationWindow.oldRequestedWidth
136+// requestedHeight: applicationWindow.oldRequestedHeight
137+ width: MathUtils.linearAnimation(0, 1, applicationWindow.requestedWidth, applicationWindow.minSize, root.scaleToPreviewProgress)
138+ height: MathUtils.linearAnimation(0, 1, applicationWindow.requestedHeight, applicationWindow.minSize, root.scaleToPreviewProgress)
138139 itemScale: root.implicitWidth / width
139140 }
140141 }
@@ -173,10 +174,10 @@ FocusScope {
173174 height: implicitHeight
174175 requestedHeight: !counterRotate ? root.requestedHeight - d.requestedDecorationHeight : root.requestedWidth
175176 requestedWidth: !counterRotate ? root.requestedWidth : root.requestedHeight - d.requestedDecorationHeight
176− property int oldRequestedWidth: requestedWidth
177− property int oldRequestedHeight: requestedHeight
178− onRequestedWidthChanged: oldRequestedWidth = requestedWidth
179− onRequestedHeightChanged: oldRequestedHeight = requestedHeight
177+// property int oldRequestedWidth: requestedWidth
178+// property int oldRequestedHeight: requestedHeight
179+// onRequestedWidthChanged: oldRequestedWidth = requestedWidth
180+// onRequestedHeightChanged: oldRequestedHeight = requestedHeight
180181 focus: true
181182
182183 property real itemScale: 1
@@ -219,6 +220,7 @@ FocusScope {
219220
220221 title: applicationWindow.title
221222 windowMoving: moveHandler.moving && !altDragHandler.dragging
223+ panelState: root.panelState
222224
223225 opacity: root.hasDecoration ? Math.min(1, root.showDecoration) : 0
224226 Behavior on opacity { UbuntuNumberAnimation { } }
@@ -242,7 +244,7 @@ FocusScope {
242244 enableMenus: {
243245 return active &&
244246 surface &&
245− (PanelState.focusedPersistentSurfaceId === surface.persistentId && !PanelState.decorationsVisible)
247+ (panelState.focusedPersistentSurfaceId === surface.persistentId && !panelState.decorationsVisible)
246248 }
247249 menu: sharedAppModel.model
248250
qml/Stage/FakeMaximizeDelegate.qml+12−11
@@ -35,6 +35,7 @@ Rectangle {
3535 property int leftMargin
3636 property real appContainerWidth
3737 property real appContainerHeight
38+ property PanelState panelState
3839
3940 readonly property real hintThreshold: 0.1
4041
@@ -178,7 +179,7 @@ Rectangle {
178179 ParallelAnimation {
179180 id: fakeMaximizeAnimation
180181 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: "y"; duration: UbuntuAnimation.BriskDuration; to: panelState.panelHeight }
182183 UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: appContainerWidth - leftMargin }
183184 UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: appContainerHeight }
184185 }
@@ -186,39 +187,39 @@ Rectangle {
186187 ParallelAnimation {
187188 id: fakeMaximizeLeftAnimation
188189 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: "y"; duration: UbuntuAnimation.BriskDuration; to: panelState.panelHeight }
190191 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+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: appContainerHeight - panelState.panelHeight }
192193 }
193194
194195 ParallelAnimation {
195196 id: fakeMaximizeRightAnimation
196197 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: "y"; duration: UbuntuAnimation.BriskDuration; to: panelState.panelHeight }
198199 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+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: appContainerHeight - panelState.panelHeight }
200201 }
201202
202203 ParallelAnimation {
203204 id: fakeMaximizeTopLeftAnimation
204205 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: "y"; duration: UbuntuAnimation.BriskDuration; to: panelState.panelHeight }
206207 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+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: (appContainerHeight - panelState.panelHeight)/2 }
208209 }
209210
210211 ParallelAnimation {
211212 id: fakeMaximizeTopRightAnimation
212213 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: "y"; duration: UbuntuAnimation.BriskDuration; to: panelState.panelHeight }
214215 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+ UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: (appContainerHeight - panelState.panelHeight)/2 }
216217 }
217218
218219 ParallelAnimation {
219220 id: fakeMaximizeBottomLeftAnimation
220221 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: "y"; duration: UbuntuAnimation.BriskDuration; to: (appContainerHeight + panelState.panelHeight)/2 }
222223 UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth - leftMargin)/2 }
223224 UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: appContainerHeight/2 }
224225 }
@@ -226,7 +227,7 @@ Rectangle {
226227 ParallelAnimation {
227228 id: fakeMaximizeBottomRightAnimation
228229 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: "y"; duration: UbuntuAnimation.BriskDuration; to: (appContainerHeight + panelState.panelHeight)/2 }
230231 UbuntuNumberAnimation { target: fakeRectangle; properties: "width"; duration: UbuntuAnimation.BriskDuration; to: (appContainerWidth - leftMargin)/2 }
231232 UbuntuNumberAnimation { target: fakeRectangle; properties: "height"; duration: UbuntuAnimation.BriskDuration; to: appContainerHeight/2 }
232233 }
qml/Stage/Spread/ScreensAndWorkspaces.qmlnew file+261
@@ -0,0 +1,261 @@
1+import QtQuick 2.4
2+import Ubuntu.Components 1.3
3+import Ubuntu.Components.Popups 1.3
4+import WindowManager 1.0
5+import Unity.Application 0.1
6+import ".."
7+
8+Item {
9+ id: root
10+
11+ property string background
12+
13+ property var screensProxy: Screens.createProxy();
14+
15+ property QtObject activeWorkspace: null
16+
17+ signal closeSpread();
18+
19+ Row {
20+ id: row
21+ anchors.bottom: parent.bottom
22+ anchors.horizontalCenter: parent.horizontalCenter
23+ Behavior on anchors.horizontalCenterOffset { NumberAnimation { duration: UbuntuAnimation.SlowDuration } }
24+ spacing: units.gu(1)
25+
26+ property var selectedIndex: undefined
27+
28+ Repeater {
29+ model: screensProxy
30+
31+ delegate: Item {
32+ height: root.height - units.gu(6)
33+ width: workspaces.width
34+
35+ Item {
36+ id: header
37+ anchors { left: parent.left; top: parent.top; right: parent.right }
38+ height: units.gu(7)
39+ z: 1
40+
41+ property bool isCurrent: {
42+ // another screen is selected.
43+ if (row.selectedIndex != undefined && row.selectedIndex != index) return false;
44+
45+ // this screen is active.
46+ if (WMScreen.active && WMScreen.isSameAs(model.screen) && WMScreen.currentWorkspace.isSameAs(activeWorkspace)) return true;
47+ if (model.screen.workspaces.indexOf(activeWorkspace) >= 0) return true;
48+
49+ // not active.
50+ return false;
51+ }
52+
53+ property bool isSelected: screenMA.containsMouse
54+ onIsSelectedChanged: {
55+ if (isSelected) {
56+ row.selectedIndex = Qt.binding(function() { return index; });
57+ } else if (row.selectedIndex === index) {
58+ row.selectedIndex = undefined;
59+ }
60+ }
61+
62+ UbuntuShape {
63+ anchors.fill: parent
64+ backgroundColor: "white"
65+ opacity: header.isCurrent || header.isSelected ? 1.0 : 0.5
66+ }
67+
68+ DropArea {
69+ anchors.fill: parent
70+ keys: ["workspace"]
71+
72+ onEntered: {
73+ workspaces.workspaceModel.insert(workspaces.workspaceModel.count, {text: drag.source.text})
74+ drag.source.inDropArea = true;
75+ }
76+
77+ onExited: {
78+ workspaces.workspaceModel.remove(workspaces.workspaceModel.count - 1, 1)
79+ drag.source.inDropArea = false;
80+ }
81+
82+ onDropped: {
83+ drag.source.inDropArea = false;
84+ }
85+ }
86+
87+ Column {
88+ anchors.fill: parent
89+ anchors.margins: units.gu(1)
90+
91+ Label {
92+ text: model.screen.name
93+ color: header.isCurrent || header.isSelected ? "black" : "white"
94+ }
95+
96+ Label {
97+ text: model.screen.outputTypeName
98+ color: header.isCurrent || header.isSelected ? "black" : "white"
99+ fontSize: "x-small"
100+ }
101+
102+ Label {
103+ text: screen.availableModes[screen.currentModeIndex].size.width + "x" + screen.availableModes[screen.currentModeIndex].size.height
104+ color: header.isCurrent || header.isSelected ? "black" : "white"
105+ fontSize: "x-small"
106+ }
107+ }
108+
109+ Icon {
110+ anchors {
111+ top: parent.top
112+ right: parent.right
113+ margins: units.gu(1)
114+ }
115+ width: units.gu(3)
116+ height: width
117+ source: "image://theme/select"
118+ color: header.isCurrent || header.isSelected ? "black" : "white"
119+ visible: model.screen.active
120+ }
121+
122+ MouseArea {
123+ id: screenMA
124+ hoverEnabled: true
125+ anchors.fill: parent
126+
127+ onClicked: {
128+ var obj = screensMenuComponent.createObject(header)
129+ obj.open(mouseX, mouseY)
130+ }
131+ }
132+
133+ Component {
134+ id: screensMenuComponent
135+ UbuntuShape {
136+ id: screensMenu
137+ width: units.gu(20)
138+ height: contentColumn.childrenRect.height
139+ backgroundColor: "white"
140+
141+ function open(mouseX, mouseY) {
142+ x = Math.max(0, Math.min(mouseX - width / 2, parent.width - width))
143+ y = mouseY + units.gu(1)
144+ }
145+
146+ InverseMouseArea {
147+ anchors.fill: parent
148+ onClicked: {
149+ screensMenu.destroy()
150+ }
151+ }
152+
153+ Column {
154+ id: contentColumn
155+ width: parent.width
156+ ListItem {
157+ height: layout.height
158+ highlightColor: "transparent"
159+ ListItemLayout {
160+ id: layout
161+ title.text: qsTr("Add workspace")
162+ title.color: "black"
163+ }
164+ onClicked: {
165+ screen.workspaces.addWorkspace();
166+ Screens.sync(root.screensProxy);
167+ screensMenu.destroy();
168+ }
169+ }
170+ }
171+ }
172+ }
173+ }
174+
175+ Workspaces {
176+ id: workspaces
177+ height: parent.height - header.height - units.gu(2)
178+ width: {
179+ var width = 0;
180+ if (screensProxy.count == 1) {
181+ width = Math.min(implicitWidth, root.width - units.gu(8));
182+ } else {
183+ width = Math.min(implicitWidth, model.screen.active ? root.width - units.gu(48) : units.gu(40))
184+ }
185+ return Math.max(workspaces.minimumWidth, width);
186+ }
187+
188+ Behavior on width { UbuntuNumberAnimation {} }
189+ anchors.bottom: parent.bottom
190+ anchors.bottomMargin: units.gu(1)
191+ anchors.horizontalCenter: parent.horizontalCenter
192+ screen: model.screen
193+ background: root.background
194+
195+ workspaceModel: model.screen.workspaces
196+ activeWorkspace: root.activeWorkspace
197+ readOnly: false
198+
199+ onCommitScreenSetup: Screens.sync(root.screensProxy)
200+ onCloseSpread: root.closeSpread();
201+
202+ onClicked: {
203+ root.activeWorkspace = workspace;
204+ }
205+ }
206+ }
207+ }
208+ }
209+
210+ Rectangle {
211+ anchors { left: parent.left; top: parent.top; bottom: parent.bottom; topMargin: units.gu(6); bottomMargin: units.gu(1) }
212+ width: units.gu(5)
213+ color: "#33000000"
214+ visible: (row.width - root.width + units.gu(10)) / 2 - row.anchors.horizontalCenterOffset > units.gu(5)
215+ MouseArea {
216+ id: leftScrollArea
217+ anchors.fill: parent
218+ hoverEnabled: true
219+ onPressed: mouse.accepted = false;
220+ }
221+ DropArea {
222+ id: leftFakeDropArea
223+ anchors.fill: parent
224+ keys: ["application", "workspace"]
225+ }
226+ }
227+ Rectangle {
228+ anchors { right: parent.right; top: parent.top; bottom: parent.bottom; topMargin: units.gu(6); bottomMargin: units.gu(1) }
229+ width: units.gu(5)
230+ color: "#33000000"
231+ visible: (row.width - root.width + units.gu(10)) / 2 + row.anchors.horizontalCenterOffset > units.gu(5)
232+ MouseArea {
233+ id: rightScrollArea
234+ anchors.fill: parent
235+ hoverEnabled: true
236+ onPressed: mouse.accepted = false;
237+ }
238+ DropArea {
239+ id: rightFakeDropArea
240+ anchors.fill: parent
241+ keys: ["application", "workspace"]
242+ }
243+ }
244+ Timer {
245+ repeat: true
246+ running: leftScrollArea.containsMouse || rightScrollArea.containsMouse || leftFakeDropArea.containsDrag || rightFakeDropArea.containsDrag
247+ interval: UbuntuAnimation.SlowDuration
248+ triggeredOnStart: true
249+ onTriggered: {
250+ var newOffset = row.anchors.horizontalCenterOffset;
251+ var maxOffset = Math.max((row.width - root.width + units.gu(10)) / 2, 0);
252+ if (leftScrollArea.containsMouse || leftFakeDropArea.containsDrag) {
253+ newOffset += units.gu(20)
254+ } else {
255+ newOffset -= units.gu(20)
256+ }
257+ newOffset = Math.max(-maxOffset, Math.min(maxOffset, newOffset));
258+ row.anchors.horizontalCenterOffset = newOffset;
259+ }
260+ }
261+}
qml/Stage/Spread/Spread.qml+7−8
@@ -28,10 +28,10 @@ Item {
2828 property var spreadFlickable
2929
3030 // some config options
31− property real contentMargin: 0.16 * root.height
32− property real contentTopMargin: contentMargin
33− property real contentBottomMargin: 0.35 * contentMargin
34− property real windowTitleTopMargin: 3/4 * (contentTopMargin - windowTitle.height)
31+ property real contentMargin: 0.1 * root.height
32+ property real contentTopMargin: contentMargin + root.y + windowTitle.height
33+ property real contentBottomMargin: contentMargin
34+ property real windowTitleTopMargin: contentMargin - windowTitle.height
3535 property int stackItemCount: 3
3636 property real leftRotationAngle: 22
3737 property real rightRotationAngle: 32
@@ -52,7 +52,7 @@ Item {
5252
5353 readonly property real spreadWidth: rightStackXPos - leftStackXPos
5454 readonly property real spreadHeight: root.height
55− readonly property real spreadItemHeight: spreadHeight - contentTopMargin - contentBottomMargin
55+ readonly property real spreadItemHeight: spreadHeight - contentMargin * 2
5656 readonly property real spreadItemWidth: stackHeight
5757
5858 readonly property real dynamicLeftRotationAngle: leftRotationAngle * rotationAngleFactor
@@ -92,10 +92,9 @@ Item {
9292
9393 readonly property real visibleItemCount: (spreadWidth / spreadItemWidth) / (1 - itemOverlap)
9494
95− readonly property real spreadTotalWidth: Math.max(2,totalItemCount) * spreadWidth / visibleItemCount
96−
97− readonly property real centeringOffset: Math.max(spreadWidth - spreadTotalWidth ,0) / (2 * spreadWidth)
95+ readonly property real spreadTotalWidth: totalItemCount * spreadWidth / visibleItemCount
9896
97+ readonly property real centeringOffset: Math.max(spreadWidth - spreadTotalWidth + (leftStackXPos - leftMargin) * 2, 0) / (2 * spreadWidth)
9998
10099 readonly property var curve: BezierCurve {
101100 controlPoint2: {'x': 0.19, 'y': 0.00}
qml/Stage/Spread/SpreadDelegateInputArea.qml+36−17
@@ -28,6 +28,9 @@ Item {
2828
2929 readonly property alias distance: d.distance
3030
31+ property var stage: null
32+ property var dragDelegate: null
33+
3134 signal clicked()
3235 signal close()
3336
@@ -84,33 +87,27 @@ Item {
8487 }
8588 }
8689
87− // Event eater
88− MouseArea {
89− anchors.fill: parent
90− onClicked: root.clicked()
91− onWheel: wheel.accepted = true
92− }
93−
9490 MultiPointTouchArea {
9591 anchors.fill: parent
96− mouseEnabled: false
9792 maximumTouchPoints: 1
9893 property int offset: 0
9994
95+ // tp.startY seems to be broken for mouse interaction... lets track it ourselves
96+ property int startY: 0
97+
10098 touchPoints: [
10199 TouchPoint {
102100 id: tp
103101 }
104102 ]
105103
106− onCanceled: {
107− d.moving = false
108− animation.animate("center");
104+ onPressed: {
105+ startY = tp.y
109106 }
110107
111108 onTouchUpdated: {
112− if (!d.moving) {
113− if (Math.abs(tp.startY - tp.y) > d.threshold) {
109+ if (!d.moving || !tp.pressed) {
110+ if (Math.abs(startY - tp.y) > d.threshold) {
114111 d.moving = true;
115112 d.dragEvents = []
116113 offset = tp.y - tp.startY;
@@ -119,17 +116,32 @@ Item {
119116 }
120117 }
121118
122− if (root.closeable) {
123− d.distance = tp.y - tp.startY - offset
119+
120+ var value = tp.y - tp.startY - offset;
121+ if (value < 0) {
122+ var coords = mapToItem(stage, tp.x, tp.y);
123+ dragDelegate.Drag.hotSpot.x = dragDelegate.width / 2
124+ dragDelegate.Drag.hotSpot.y = units.gu(2)
125+ dragDelegate.x = coords.x - dragDelegate.Drag.hotSpot.x
126+ dragDelegate.y = coords.y - dragDelegate.Drag.hotSpot.y
127+ dragDelegate.Drag.active = true;
128+ dragDelegate.surface = model.window.surface;
129+
124130 } else {
125− var value = tp.y - tp.startY - offset;
126− d.distance = Math.sqrt(Math.abs(value)) * (value < 0 ? -1 : 1) * 3
131+ if (root.closeable) {
132+ d.distance = value
133+ } else {
134+ d.distance = Math.sqrt(Math.abs(value)) * (value < 0 ? -1 : 1) * 3
135+ }
127136 }
128137
129138 d.pushDragEvent(tp);
130139 }
131140
132141 onReleased: {
142+ var result = dragDelegate.Drag.drop();
143+ dragDelegate.surface = null;
144+
133145 if (!d.moving) {
134146 root.clicked()
135147 }
@@ -149,6 +161,13 @@ Item {
149161 animation.animate("center")
150162 }
151163 }
164+
165+ onCanceled: {
166+ dragDelegate.Drag.active = false;
167+ dragDelegate.surface = null;
168+ d.moving = false
169+ animation.animate("center");
170+ }
152171 }
153172
154173 UbuntuNumberAnimation {
qml/Stage/Spread/WorkspacePreview.qmlnew file+127
@@ -0,0 +1,127 @@
1+import QtQuick 2.4
2+import Ubuntu.Components 1.3
3+import Unity.Application 0.1
4+import WindowManager 1.0
5+import ".."
6+import "../../Components"
7+
8+Item {
9+ id: previewSpace
10+ clip: true
11+
12+ property var workspace
13+
14+ property string background
15+ property int screenHeight
16+
17+ property real previewScale: previewSpace.height / previewSpace.screenHeight
18+
19+ property bool containsDragLeft: false
20+ property bool containsDragRight: false
21+ property bool isActive: false
22+ property bool isSelected: false
23+
24+ Image {
25+ source: previewSpace.background
26+ anchors.fill: parent
27+ sourceSize.width: width
28+ sourceSize.height: height
29+
30+ Repeater {
31+ id: topLevelSurfaceRepeater
32+ model: visible ? workspace.windowModel : null
33+ delegate: Item {
34+ width: surfaceItem.width
35+ height: surfaceItem.height + decorationHeight * previewScale
36+ x: model.window.position.x * previewScale
37+ y: (model.window.position.y - decorationHeight) * previewScale
38+ z: topLevelSurfaceRepeater.count - index
39+ visible: model.window.state !== Mir.MinimizedState && model.window.state !== Mir.HiddenState
40+
41+ property int decorationHeight: units.gu(3)
42+
43+ WindowDecoration {
44+ width: surfaceItem.implicitWidth
45+ height: parent.decorationHeight
46+ transform: Scale {
47+ origin.x: 0
48+ origin.y: 0
49+ xScale: previewScale
50+ yScale: previewScale
51+ }
52+ title: model.window && model.window.surface ? model.window.surface.name : ""
53+ z: 3
54+ }
55+
56+ MirSurfaceItem {
57+ id: surfaceItem
58+ y: parent.decorationHeight * previewScale
59+ width: implicitWidth * previewScale
60+ height: implicitHeight * previewScale
61+ surfaceWidth: -1
62+ surfaceHeight: -1
63+ surface: model.window.surface
64+ }
65+ }
66+ }
67+
68+ }
69+
70+ Rectangle {
71+ anchors.fill: parent
72+ border.color: UbuntuColors.ash
73+ border.width: units.gu(.5)
74+ color: "transparent"
75+ visible: previewSpace.isActive
76+ }
77+
78+ Rectangle {
79+ anchors.fill: parent
80+ border.color: UbuntuColors.blue
81+ border.width: units.gu(.5)
82+ color: "transparent"
83+ visible: previewSpace.isSelected
84+ }
85+
86+ Rectangle {
87+ anchors.fill: parent
88+ anchors.rightMargin: parent.width / 2
89+ color: "#55000000"
90+ visible: previewSpace.containsDragLeft
91+
92+ Column {
93+ anchors.centerIn: parent
94+ spacing: units.gu(1)
95+ Icon {
96+ source: "../graphics/multi-monitor_drop-here.png"
97+ height: units.gu(4)
98+ width: height
99+ anchors.horizontalCenter: parent.horizontalCenter
100+ }
101+ Label {
102+ text: qsTr("Drop here")
103+ }
104+ }
105+ }
106+
107+ Rectangle {
108+ anchors.fill: parent
109+ anchors.leftMargin: parent.width / 2
110+ color: "#55000000"
111+ visible: previewSpace.containsDragRight
112+
113+ Column {
114+ anchors.centerIn: parent
115+ spacing: units.gu(1)
116+ Icon {
117+ source: "../graphics/multi-monitor_leave.png"
118+ height: units.gu(4)
119+ width: height
120+ anchors.horizontalCenter: parent.horizontalCenter
121+ }
122+ Label {
123+ text: qsTr("Drop and go")
124+ }
125+ }
126+ }
127+}
qml/Stage/Spread/Workspaces.qmlnew file+412
@@ -0,0 +1,412 @@
1+import QtQuick 2.4
2+import Ubuntu.Components 1.3
3+import WindowManager 1.0
4+import "MathUtils.js" as MathUtils
5+import "../../Components"
6+
7+Item {
8+ id: root
9+ implicitWidth: listView.contentWidth
10+ readonly property int minimumWidth: {
11+ var count = Math.min(3, listView.count);
12+ return listView.itemWidth * count + listView.spacing * (count - 1)
13+ }
14+
15+ property QtObject screen: null
16+ property alias workspaceModel: listView.model
17+ property var background // TODO: should be stored in the workspace data
18+ property int selectedIndex: -1
19+ property bool readOnly: true
20+ property var activeWorkspace: null
21+
22+ signal commitScreenSetup();
23+ signal closeSpread();
24+ signal clicked(var workspace);
25+
26+ DropArea {
27+ anchors.fill: root
28+
29+ keys: ['workspace']
30+
31+ onEntered: {
32+ var index = listView.getDropIndex(drag);
33+ drag.source.workspace.assign(workspaceModel, index)
34+ drag.source.inDropArea = true;
35+ }
36+
37+ onPositionChanged: {
38+ var index = listView.getDropIndex(drag);
39+ if (listView.dropItemIndex == index) return;
40+ listView.model.move(listView.dropItemIndex, index, 1);
41+ listView.dropItemIndex = index;
42+ }
43+
44+ onExited: {
45+ drag.source.workspace.unassign()
46+ listView.dropItemIndex = -1;
47+ listView.hoveredWorkspaceIndex = -1;
48+ drag.source.inDropArea = false;
49+ }
50+
51+ onDropped: {
52+ drop.accept(Qt.MoveAction);
53+ listView.dropItemIndex = -1;
54+ drag.source.inDropArea = false;
55+ }
56+ }
57+ DropArea {
58+ anchors.fill: parent
59+ keys: ["application"]
60+
61+ onPositionChanged: {
62+ listView.progressiveScroll(drag.x)
63+ listView.updateDropProperties(drag)
64+ }
65+ onExited: {
66+ listView.hoveredWorkspaceIndex = -1
67+ }
68+ onDropped: {
69+ var surface = drag.source.surface;
70+ drag.source.surface = null;
71+ var workspace = listView.model.get(listView.hoveredWorkspaceIndex);
72+ WorkspaceManager.moveSurfaceToWorkspace(surface, workspace);
73+ drop.accept(Qt.MoveAction)
74+ if (listView.hoveredHalf == "right") {
75+ root.closeSpread();
76+ workspace.activate();
77+ }
78+ surface.activate();
79+ listView.hoveredWorkspaceIndex = -1
80+ }
81+ }
82+
83+ onSelectedIndexChanged: {
84+ listView.positionViewAtIndex(selectedIndex, ListView.Center);
85+ }
86+
87+ Item {
88+ // We need to clip the listview as it has left/right margins and it would
89+ // overlap with items next to it and eat mouse input. However, we can't
90+ // just clip at the actual bounds as the delegates have the close button
91+ // on hover which reaches a bit outside, so lets some margins for the clipping
92+ anchors.fill: parent
93+ anchors.margins: -units.gu(2)
94+ clip: true
95+
96+
97+ ListView {
98+ id: listView
99+ anchors {
100+ fill: parent
101+ topMargin: -parent.anchors.margins
102+ bottomMargin: -parent.anchors.margins
103+ leftMargin: -itemWidth - parent.anchors.margins
104+ rightMargin: -itemWidth - parent.anchors.margins
105+ }
106+ boundsBehavior: Flickable.StopAtBounds
107+
108+ Behavior on contentX {
109+ SmoothedAnimation { duration: 200 }
110+ }
111+
112+ property var clickedWorkspace: null
113+
114+ orientation: ListView.Horizontal
115+ spacing: units.gu(1)
116+ leftMargin: itemWidth
117+ rightMargin: itemWidth
118+
119+ property int screenWidth: screen.availableModes[screen.currentModeIndex].size.width
120+ property int screenHeight: screen.availableModes[screen.currentModeIndex].size.height
121+ property int itemWidth: height * screenWidth / screenHeight
122+ property int foldingAreaWidth: itemWidth / 2
123+ property int maxAngle: 40
124+
125+ property real realContentX: contentX - originX + leftMargin
126+ property int dropItemIndex: -1
127+ property int hoveredWorkspaceIndex: -1
128+ property string hoveredHalf: "" // left or right
129+
130+ function getDropIndex(drag) {
131+ var coords = mapToItem(listView.contentItem, drag.x, drag.y)
132+ var index = Math.floor((drag.x + listView.realContentX) / (listView.itemWidth + listView.spacing));
133+ if (index < 0) index = 0;
134+ var upperLimit = dropItemIndex == -1 ? listView.count : listView.count - 1
135+ if (index > upperLimit) index = upperLimit;
136+ return index;
137+ }
138+
139+ function updateDropProperties(drag) {
140+ var coords = mapToItem(listView.contentItem, drag.x, drag.y)
141+ var index = Math.floor(drag.x + listView.realContentX) / (listView.itemWidth + listView.spacing);
142+ if (index < 0) {
143+ listView.hoveredWorkspaceIndex = -1;
144+ listView.hoveredHalf = "";
145+ return;
146+ }
147+
148+ var upperLimit = dropItemIndex == -1 ? listView.count : listView.count - 1
149+ if (index > upperLimit) index = upperLimit;
150+ listView.hoveredWorkspaceIndex = index;
151+ var pixelsInTile = (drag.x + listView.realContentX) % (listView.itemWidth + listView.spacing);
152+ listView.hoveredHalf = (pixelsInTile / listView.itemWidth) < .5 ? "left" : "right";
153+ }
154+
155+ function progressiveScroll(mouseX) {
156+ var progress = Math.max(0, Math.min(1, (mouseX - listView.itemWidth) / (width - listView.leftMargin * 2 - listView.itemWidth * 2)))
157+ listView.contentX = listView.originX + (listView.contentWidth - listView.width + listView.leftMargin + listView.rightMargin) * progress - listView.leftMargin
158+ }
159+
160+ displaced: Transition { UbuntuNumberAnimation { properties: "x" } }
161+
162+ delegate: Item {
163+ id: workspaceDelegate
164+ objectName: "delegate" + index
165+ height: parent.height
166+ width: listView.itemWidth
167+ Behavior on width { UbuntuNumberAnimation {} }
168+ visible: listView.dropItemIndex !== index
169+
170+ property int itemX: -listView.realContentX + index * (listView.itemWidth + listView.spacing)
171+ property int distanceFromLeft: itemX //- listView.leftMargin
172+ property int distanceFromRight: listView.width - listView.leftMargin - listView.rightMargin - itemX - listView.itemWidth
173+
174+ property int itemAngle: {
175+ if (index == 0) {
176+ if (distanceFromLeft < 0) {
177+ var progress = (distanceFromLeft + listView.foldingAreaWidth) / listView.foldingAreaWidth
178+ return MathUtils.linearAnimation(1, -1, 0, listView.maxAngle, Math.max(-1, Math.min(1, progress)));
179+ }
180+ return 0
181+ }
182+ if (index == listView.count - 1) {
183+ if (distanceFromRight < 0) {
184+ var progress = (distanceFromRight + listView.foldingAreaWidth) / listView.foldingAreaWidth
185+ return MathUtils.linearAnimation(1, -1, 0, -listView.maxAngle, Math.max(-1, Math.min(1, progress)));
186+ }
187+ return 0
188+ }
189+
190+ if (distanceFromLeft < listView.foldingAreaWidth) {
191+ // itemX : 10gu = p : 100
192+ var progress = distanceFromLeft / listView.foldingAreaWidth
193+ return MathUtils.linearAnimation(1, -1, 0, listView.maxAngle, Math.max(-1, Math.min(1, progress)));
194+ }
195+ if (distanceFromRight < listView.foldingAreaWidth) {
196+ var progress = distanceFromRight / listView.foldingAreaWidth
197+ return MathUtils.linearAnimation(1, -1, 0, -listView.maxAngle, Math.max(-1, Math.min(1, progress)));
198+ }
199+ return 0
200+ }
201+
202+ property int itemOffset: {
203+ if (index == 0) {
204+ if (distanceFromLeft < 0) {
205+ return -distanceFromLeft
206+ }
207+ return 0
208+ }
209+ if (index == listView.count - 1) {
210+ if (distanceFromRight < 0) {
211+ return distanceFromRight
212+ }
213+ return 0
214+ }
215+
216+ if (itemX < -listView.foldingAreaWidth) {
217+ return -itemX
218+ }
219+ if (distanceFromLeft < listView.foldingAreaWidth) {
220+ return (listView.foldingAreaWidth - distanceFromLeft) / 2
221+ }
222+
223+ if (distanceFromRight < -listView.foldingAreaWidth) {
224+ return distanceFromRight
225+ }
226+
227+ if (distanceFromRight < listView.foldingAreaWidth) {
228+ return -(listView.foldingAreaWidth - distanceFromRight) / 2
229+ }
230+
231+ return 0
232+ }
233+
234+ z: itemOffset < 0 ? itemOffset : -itemOffset
235+ transform: [
236+ Rotation {
237+ angle: itemAngle
238+ axis { x: 0; y: 1; z: 0 }
239+ origin { x: itemAngle < 0 ? listView.itemWidth : 0; y: height / 2 }
240+ },
241+ Translate {
242+ x: itemOffset
243+ }
244+ ]
245+
246+ WorkspacePreview {
247+ id: workspacePreview
248+ height: listView.height
249+ width: listView.itemWidth
250+ background: root.background
251+ screenHeight: listView.screenHeight
252+ containsDragLeft: listView.hoveredWorkspaceIndex == index && listView.hoveredHalf == "left"
253+ containsDragRight: listView.hoveredWorkspaceIndex == index && listView.hoveredHalf == "right"
254+ isActive: workspace.isSameAs(root.activeWorkspace)
255+ isSelected: index === root.selectedIndex
256+ workspace: model.workspace
257+ }
258+ MouseArea {
259+ anchors.fill: parent
260+ onClicked: {
261+ root.clicked(model.workspace)
262+ }
263+ onDoubleClicked: {
264+ model.workspace.activate();
265+ root.closeSpread();
266+ }
267+ }
268+
269+ MouseArea {
270+ id: closeMouseArea
271+ objectName: "closeMouseArea"
272+ anchors { left: parent.left; top: parent.top; leftMargin: -height / 2; topMargin: -height / 2 }
273+ hoverEnabled: true
274+ height: units.gu(4)
275+ width: height
276+ visible: !root.readOnly && listView.count > 1
277+
278+ onClicked: {
279+ model.workspace.unassign();
280+ root.commitScreenSetup();
281+ }
282+ Image {
283+ id: closeImage
284+ source: "../graphics/window-close.svg"
285+ anchors.fill: closeMouseArea
286+ anchors.margins: units.gu(1)
287+ sourceSize.width: width
288+ sourceSize.height: height
289+ readonly property var mousePos: hoverMouseArea.mapToItem(workspaceDelegate, hoverMouseArea.mouseX, hoverMouseArea.mouseY)
290+ readonly property bool shown: (hoverMouseArea.containsMouse || parent.containsMouse)
291+ && mousePos.y < workspaceDelegate.width / 4
292+ && mousePos.y > -units.gu(2)
293+ && mousePos.x > -units.gu(2)
294+ && mousePos.x < workspaceDelegate.height / 4
295+ opacity: shown ? 1 : 0
296+ visible: opacity > 0
297+ Behavior on opacity { UbuntuNumberAnimation { duration: UbuntuAnimation.SnapDuration } }
298+
299+ }
300+ }
301+ }
302+
303+ MouseArea {
304+ id: hoverMouseArea
305+ anchors.fill: parent
306+ hoverEnabled: true
307+ propagateComposedEvents: true
308+ anchors.leftMargin: listView.leftMargin
309+ anchors.rightMargin: listView.rightMargin
310+ enabled: !root.readOnly
311+
312+ property int draggedIndex: -1
313+
314+ property int startX: 0
315+ property int startY: 0
316+
317+ onMouseXChanged: {
318+ if (!pressed || dragging) {
319+ listView.progressiveScroll(mouseX)
320+ }
321+ }
322+ onMouseYChanged: {
323+ if (Math.abs(mouseY - startY) > units.gu(3)) {
324+ drag.axis = Drag.XAndYAxis;
325+ }
326+ }
327+
328+ onReleased: {
329+ var result = fakeDragItem.Drag.drop();
330+ // if (result == Qt.IgnoreAction) {
331+ // WorkspaceManager.destroyWorkspace(fakeDragItem.workspace);
332+ // }
333+ root.commitScreenSetup();
334+ drag.target = null;
335+ }
336+
337+ property bool dragging: drag.active
338+ onDraggingChanged: {
339+ if (drag.active) {
340+ var ws = listView.model.get(draggedIndex);
341+ if (ws) ws.unassign();
342+ }
343+ }
344+
345+ onPressed: {
346+ startX = mouseX;
347+ startY = mouseY;
348+ if (listView.model.count < 2) return;
349+
350+ var coords = mapToItem(listView.contentItem, mouseX, mouseY)
351+ draggedIndex = listView.indexAt(coords.x, coords.y)
352+ var clickedItem = listView.itemAt(coords.x, coords.y)
353+
354+ var itemCoords = clickedItem.mapToItem(listView, -listView.leftMargin, 0);
355+ fakeDragItem.x = itemCoords.x
356+ fakeDragItem.y = itemCoords.y
357+ fakeDragItem.workspace = listView.model.get(draggedIndex)
358+
359+ var mouseCoordsInItem = mapToItem(clickedItem, mouseX, mouseY);
360+ fakeDragItem.Drag.hotSpot.x = mouseCoordsInItem.x
361+ fakeDragItem.Drag.hotSpot.y = mouseCoordsInItem.y
362+
363+ drag.axis = Drag.YAxis;
364+ drag.target = fakeDragItem;
365+ }
366+
367+ WorkspacePreview {
368+ id: fakeDragItem
369+ height: listView.height
370+ width: listView.itemWidth
371+ background: root.background
372+ screenHeight: screen.availableModes[screen.currentModeIndex].size.height
373+ visible: Drag.active
374+
375+ Drag.active: hoverMouseArea.drag.active
376+ Drag.keys: ['workspace']
377+
378+ property bool inDropArea: false
379+
380+ Rectangle {
381+ anchors.fill: parent
382+ color: "#33000000"
383+ opacity: parent.inDropArea ? 0 : 1
384+ Behavior on opacity { UbuntuNumberAnimation { } }
385+ Rectangle {
386+ anchors.centerIn: parent
387+ width: units.gu(6)
388+ height: units.gu(6)
389+ radius: width / 2
390+ color: "#aa000000"
391+ }
392+
393+ Icon {
394+ height: units.gu(3)
395+ width: height
396+ anchors.centerIn: parent
397+ name: "edit-delete"
398+ color: "white"
399+ }
400+ }
401+
402+ states: [
403+ State {
404+ when: fakeDragItem.Drag.active
405+ ParentChange { target: fakeDragItem; parent: shell }
406+ }
407+ ]
408+ }
409+ }
410+ }
411+ }
412+}
qml/Stage/Stage.qml+223−112
@@ -48,6 +48,9 @@ FocusScope {
4848 property rect inputMethodRect
4949 property real rightEdgePushProgress: 0
5050 property Item availableDesktopArea
51+ property PanelState panelState
52+
53+ readonly property var temporarySelectedWorkspace: state == "spread" ? screensAndWorkspaces.activeWorkspace : null
5154
5255 // Configuration
5356 property string mode: "staged"
@@ -154,6 +157,7 @@ FocusScope {
154157 function updateFocusedAppOrientationAnimated() { /* TODO */}
155158
156159 function closeSpread() {
160+ spreadItem.highlightedIndex = -1;
157161 priv.goneToSpread = false;
158162 }
159163
@@ -260,6 +264,43 @@ FocusScope {
260264 }
261265 }
262266
267+ GlobalShortcut {
268+ id: showWorkspaceSwitcherShortcutLeft
269+ shortcut: Qt.AltModifier|Qt.ControlModifier|Qt.Key_Left
270+ active: !workspaceSwitcher.active
271+ onTriggered: {
272+ root.focus = true;
273+ workspaceSwitcher.showLeft()
274+ }
275+ }
276+ GlobalShortcut {
277+ id: showWorkspaceSwitcherShortcutRight
278+ shortcut: Qt.AltModifier|Qt.ControlModifier|Qt.Key_Right
279+ active: !workspaceSwitcher.active
280+ onTriggered: {
281+ root.focus = true;
282+ workspaceSwitcher.showRight()
283+ }
284+ }
285+ GlobalShortcut {
286+ id: showWorkspaceSwitcherShortcutUp
287+ shortcut: Qt.AltModifier|Qt.ControlModifier|Qt.Key_Up
288+ active: !workspaceSwitcher.active
289+ onTriggered: {
290+ root.focus = true;
291+ workspaceSwitcher.showUp()
292+ }
293+ }
294+ GlobalShortcut {
295+ id: showWorkspaceSwitcherShortcutDown
296+ shortcut: Qt.AltModifier|Qt.ControlModifier|Qt.Key_Down
297+ active: !workspaceSwitcher.active
298+ onTriggered: {
299+ root.focus = true;
300+ workspaceSwitcher.showDown()
301+ }
302+ }
303+
263304 QtObject {
264305 id: priv
265306 objectName: "DesktopStagePrivate"
@@ -395,23 +436,23 @@ FocusScope {
395436 readonly property real windowDecorationHeight: units.gu(3)
396437 }
397438
398− Component.onCompleted: priv.updateMainAndSideStageIndexes();
439+ Component.onCompleted: priv.updateMainAndSideStageIndexes()
399440
400441 Connections {
401− target: PanelState
442+ target: panelState
402443 onCloseClicked: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.close(); } }
403444 onMinimizeClicked: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.requestMinimize(); } }
404445 onRestoreClicked: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.requestRestore(); } }
405446 }
406447
407448 Binding {
408− target: PanelState
449+ target: panelState
409450 property: "decorationsVisible"
410451 value: mode == "windowed" && priv.focusedAppDelegate && priv.focusedAppDelegate.maximized && !root.spreadShown
411452 }
412453
413454 Binding {
414− target: PanelState
455+ target: panelState
415456 property: "title"
416457 value: {
417458 if (priv.focusedAppDelegate !== null) {
@@ -426,7 +467,7 @@ FocusScope {
426467 }
427468
428469 Binding {
429− target: PanelState
470+ target: panelState
430471 property: "focusedPersistentSurfaceId"
431472 value: {
432473 if (priv.focusedAppDelegate !== null) {
@@ -440,21 +481,21 @@ FocusScope {
440481 }
441482
442483 Binding {
443− target: PanelState
484+ target: panelState
444485 property: "dropShadow"
445486 value: priv.focusedAppDelegate && !priv.focusedAppDelegate.maximized && priv.foregroundMaximizedAppDelegate !== null && mode == "windowed"
446487 }
447488
448489 Binding {
449− target: PanelState
490+ target: panelState
450491 property: "closeButtonShown"
451492 value: priv.focusedAppDelegate && priv.focusedAppDelegate.maximized
452493 }
453494
454495 Component.onDestruction: {
455− PanelState.title = "";
456− PanelState.decorationsVisible = false;
457− PanelState.dropShadow = false;
496+ panelState.title = "";
497+ panelState.decorationsVisible = false;
498+ panelState.dropShadow = false;
458499 }
459500
460501 Instantiator {
@@ -500,6 +541,7 @@ FocusScope {
500541 PropertyChanges { target: noAppsRunningHint; visible: (root.topLevelSurfaceList.count < 1) }
501542 PropertyChanges { target: blurLayer; visible: true; blurRadius: 32; brightness: .65; opacity: 1 }
502543 PropertyChanges { target: wallpaper; visible: false }
544+ PropertyChanges { target: screensAndWorkspaces; opacity: 1 }
503545 },
504546 State {
505547 name: "stagedRightEdge"; when: root.spreadEnabled && (rightEdgeDragArea.dragging || rightEdgePushProgress > 0) && root.mode == "staged"
@@ -554,11 +596,16 @@ FocusScope {
554596 Transition {
555597 from: "stagedRightEdge,sideStagedRightEdge,windowedRightEdge"; to: "spread"
556598 PropertyAction { target: spreadItem; property: "highlightedIndex"; value: -1 }
599+ PropertyAction { target: screensAndWorkspaces; property: "activeWorkspace"; value: WMScreen.currentWorkspace }
557600 PropertyAnimation { target: blurLayer; properties: "brightness,blurRadius"; duration: priv.animationDuration }
601+ UbuntuNumberAnimation { target: screensAndWorkspaces; property: "opacity"; duration: priv.animationDuration }
558602 },
559603 Transition {
560604 to: "spread"
605+ PropertyAction { target: screensAndWorkspaces; property: "activeWorkspace"; value: WMScreen.currentWorkspace }
561606 PropertyAction { target: spreadItem; property: "highlightedIndex"; value: appRepeater.count > 1 ? 1 : 0 }
607+ PropertyAction { target: floatingFlickable; property: "contentX"; value: 0 }
608+ UbuntuNumberAnimation { target: screensAndWorkspaces; property: "opacity"; duration: priv.animationDuration }
562609 },
563610 Transition {
564611 from: "spread"
@@ -616,10 +663,20 @@ FocusScope {
616663 visible: false
617664 }
618665
666+ ScreensAndWorkspaces {
667+ id: screensAndWorkspaces
668+ anchors { left: parent.left; top: parent.top; right: parent.right; leftMargin: root.leftMargin }
669+ height: Math.max(units.gu(30), parent.height * .3)
670+ background: root.background
671+ opacity: 0
672+ visible: opacity > 0
673+ onCloseSpread: priv.goneToSpread = false;
674+ }
675+
619676 Spread {
620677 id: spreadItem
621678 objectName: "spreadItem"
622− anchors.fill: appContainer
679+ anchors { left: parent.left; bottom: parent.bottom; right: parent.right; top: screensAndWorkspaces.bottom }
623680 leftMargin: root.availableDesktopArea.x
624681 model: root.topLevelSurfaceList
625682 spreadFlickable: floatingFlickable
@@ -632,6 +689,89 @@ FocusScope {
632689 onCloseCurrentApp: {
633690 appRepeater.itemAt(highlightedIndex).close();
634691 }
692+
693+ FloatingFlickable {
694+ id: floatingFlickable
695+ objectName: "spreadFlickable"
696+ anchors.fill: parent
697+ enabled: false
698+ contentWidth: spreadItem.spreadTotalWidth
699+
700+ function snap(toIndex) {
701+ var delegate = appRepeater.itemAt(toIndex)
702+ var targetContentX = floatingFlickable.contentWidth / spreadItem.totalItemCount * toIndex;
703+ if (targetContentX - floatingFlickable.contentX > spreadItem.rightStackXPos - (spreadItem.spreadItemWidth / 2)) {
704+ var offset = (spreadItem.rightStackXPos - (spreadItem.spreadItemWidth / 2)) - (targetContentX - floatingFlickable.contentX)
705+ snapAnimation.to = floatingFlickable.contentX - offset;
706+ snapAnimation.start();
707+ } else if (targetContentX - floatingFlickable.contentX < spreadItem.leftStackXPos + units.gu(1)) {
708+ var offset = (spreadItem.leftStackXPos + units.gu(1)) - (targetContentX - floatingFlickable.contentX);
709+ snapAnimation.to = floatingFlickable.contentX - offset;
710+ snapAnimation.start();
711+ }
712+ }
713+ UbuntuNumberAnimation {id: snapAnimation; target: floatingFlickable; property: "contentX"}
714+ }
715+
716+ MouseArea {
717+ id: hoverMouseArea
718+ objectName: "hoverMouseArea"
719+ anchors.fill: parent
720+ propagateComposedEvents: true
721+ hoverEnabled: true
722+ enabled: false
723+ visible: enabled
724+ property bool wasTouchPress: false
725+
726+ property int scrollAreaWidth: width / 3
727+ property bool progressiveScrollingEnabled: false
728+
729+ onMouseXChanged: {
730+ mouse.accepted = false
731+
732+ if (hoverMouseArea.pressed || wasTouchPress) {
733+ return;
734+ }
735+
736+ // Find the hovered item and mark it active
737+ for (var i = appRepeater.count - 1; i >= 0; i--) {
738+ var appDelegate = appRepeater.itemAt(i);
739+ var mapped = mapToItem(appDelegate, hoverMouseArea.mouseX, hoverMouseArea.mouseY)
740+ var itemUnder = appDelegate.childAt(mapped.x, mapped.y);
741+ if (itemUnder && (itemUnder.objectName === "dragArea" || itemUnder.objectName === "windowInfoItem" || itemUnder.objectName == "closeMouseArea")) {
742+ spreadItem.highlightedIndex = i;
743+ break;
744+ }
745+ }
746+
747+ if (floatingFlickable.contentWidth > floatingFlickable.width) {
748+ var margins = floatingFlickable.width * 0.05;
749+
750+ if (!progressiveScrollingEnabled && mouseX < floatingFlickable.width - scrollAreaWidth) {
751+ progressiveScrollingEnabled = true
752+ }
753+
754+ // do we need to scroll?
755+ if (mouseX < scrollAreaWidth + margins) {
756+ var progress = Math.min(1, (scrollAreaWidth + margins - mouseX) / (scrollAreaWidth - margins));
757+ var contentX = (1 - progress) * (floatingFlickable.contentWidth - floatingFlickable.width)
758+ floatingFlickable.contentX = Math.max(0, Math.min(floatingFlickable.contentX, contentX))
759+ }
760+ if (mouseX > floatingFlickable.width - scrollAreaWidth && progressiveScrollingEnabled) {
761+ var progress = Math.min(1, (mouseX - (floatingFlickable.width - scrollAreaWidth)) / (scrollAreaWidth - margins))
762+ var contentX = progress * (floatingFlickable.contentWidth - floatingFlickable.width)
763+ floatingFlickable.contentX = Math.min(floatingFlickable.contentWidth - floatingFlickable.width, Math.max(floatingFlickable.contentX, contentX))
764+ }
765+ }
766+ }
767+
768+ onPressed: {
769+ mouse.accepted = false;
770+ wasTouchPress = mouse.source === Qt.MouseEventSynthesizedByQt;
771+ }
772+
773+ onExited: wasTouchPress = false;
774+ }
635775 }
636776
637777 Label {
@@ -739,6 +879,24 @@ FocusScope {
739879 }
740880 }
741881
882+ MirSurfaceItem {
883+ id: fakeDragItem
884+ property real previewScale: .5
885+ height: (screensAndWorkspaces.height - units.gu(8)) / 2
886+ // w : h = iw : ih
887+ width: implicitWidth * height / implicitHeight
888+ surfaceWidth: -1
889+ surfaceHeight: -1
890+ opacity: surface != null ? 1 : 0
891+ Behavior on opacity { UbuntuNumberAnimation {} }
892+ visible: opacity > 0
893+
894+ Drag.active: surface != null
895+ Drag.keys: ["application"]
896+
897+ z: 1000
898+ }
899+
742900 Repeater {
743901 id: appRepeater
744902 model: topLevelSurfaceList
@@ -767,6 +925,9 @@ FocusScope {
767925 }
768926 z: normalZ
769927
928+ opacity: fakeDragItem.surface == model.window.surface && fakeDragItem.Drag.active ? 0 : 1
929+ Behavior on opacity { UbuntuNumberAnimation {} }
930+
770931 // Normally we want x/y where the surface thinks it is. Width/height of our delegate will
771932 // match what the actual surface size is.
772933 // Don't write to those, they will be set by states
@@ -946,7 +1107,6 @@ FocusScope {
9461107 function claimFocus() {
9471108 if (root.state == "spread") {
9481109 spreadItem.highlightedIndex = index
949− priv.goneToSpread = false;
9501110 }
9511111 if (root.mode == "stagedWithSideStage") {
9521112 if (appDelegate.stage == ApplicationInfoInterface.SideStage && !sideStage.shown) {
@@ -1280,8 +1440,6 @@ FocusScope {
12801440 y: spreadMaths.targetY
12811441 z: index
12821442 height: spreadItem.spreadItemHeight
1283− requestedWidth: decoratedWindow.oldRequestedWidth
1284− requestedHeight: decoratedWindow.oldRequestedHeight
12851443 visible: spreadMaths.itemVisible
12861444 }
12871445 PropertyChanges { target: dragArea; enabled: true }
@@ -1301,8 +1459,6 @@ FocusScope {
13011459 y: stagedRightEdgeMaths.animatedY
13021460 z: stagedRightEdgeMaths.animatedZ
13031461 height: stagedRightEdgeMaths.animatedHeight
1304− requestedWidth: decoratedWindow.oldRequestedWidth
1305− requestedHeight: decoratedWindow.oldRequestedHeight
13061462 visible: appDelegate.x < root.width
13071463 }
13081464 PropertyChanges {
@@ -1332,8 +1488,6 @@ FocusScope {
13321488 y: windowedRightEdgeMaths.animatedY
13331489 z: windowedRightEdgeMaths.animatedZ
13341490 height: stagedRightEdgeMaths.animatedHeight
1335− requestedWidth: decoratedWindow.oldRequestedWidth
1336− requestedHeight: decoratedWindow.oldRequestedHeight
13371491 }
13381492 PropertyChanges {
13391493 target: decoratedWindow
@@ -1356,11 +1510,15 @@ FocusScope {
13561510 target: appDelegate
13571511 x: stageMaths.itemX
13581512 y: root.availableDesktopArea.y
1359− requestedWidth: appContainer.width
1360− requestedHeight: root.availableDesktopArea.height
13611513 visuallyMaximized: true
13621514 visible: appDelegate.x < root.width
13631515 }
1516+ PropertyChanges {
1517+ target: appDelegate
1518+ requestedWidth: appContainer.width
1519+ requestedHeight: root.availableDesktopArea.height
1520+ restoreEntryValues: false
1521+ }
13641522 PropertyChanges {
13651523 target: decoratedWindow
13661524 hasDecoration: false
@@ -1389,11 +1547,15 @@ FocusScope {
13891547 x: stageMaths.itemX
13901548 y: root.availableDesktopArea.y
13911549 z: stageMaths.itemZ
1392− requestedWidth: stageMaths.itemWidth
1393− requestedHeight: root.availableDesktopArea.height
13941550 visuallyMaximized: true
13951551 visible: appDelegate.x < root.width
13961552 }
1553+ PropertyChanges {
1554+ target: appDelegate
1555+ requestedWidth: stageMaths.itemWidth
1556+ requestedHeight: root.availableDesktopArea.height
1557+ restoreEntryValues: false
1558+ }
13971559 PropertyChanges {
13981560 target: decoratedWindow
13991561 hasDecoration: false
@@ -1414,8 +1576,13 @@ FocusScope {
14141576 requestedX: root.availableDesktopArea.x;
14151577 requestedY: 0;
14161578 visuallyMinimized: false;
1579+ visuallyMaximized: true
1580+ }
1581+ PropertyChanges {
1582+ target: appDelegate
14171583 requestedWidth: root.availableDesktopArea.width;
14181584 requestedHeight: appContainer.height;
1585+ restoreEntryValues: false
14191586 }
14201587 PropertyChanges { target: touchControls; enabled: true }
14211588 PropertyChanges { target: decoratedWindow; windowControlButtonsVisible: false }
@@ -1426,8 +1593,12 @@ FocusScope {
14261593 target: appDelegate;
14271594 requestedX: 0
14281595 requestedY: 0
1429− requestedWidth: appContainer.width;
1430− requestedHeight: appContainer.height;
1596+ }
1597+ PropertyChanges {
1598+ target: appDelegate
1599+ requestedWidth: appContainer.width
1600+ requestedHeight: appContainer.height
1601+ restoreEntryValues: false
14311602 }
14321603 PropertyChanges { target: decoratedWindow; hasDecoration: false }
14331604 },
@@ -1441,6 +1612,12 @@ FocusScope {
14411612 PropertyChanges { target: touchControls; enabled: true }
14421613 PropertyChanges { target: resizeArea; enabled: true }
14431614 PropertyChanges { target: decoratedWindow; shadowOpacity: .3; windowControlButtonsVisible: true}
1615+ PropertyChanges {
1616+ target: appDelegate
1617+ requestedWidth: windowedWidth
1618+ requestedHeight: windowedHeight
1619+ restoreEntryValues: false
1620+ }
14441621 },
14451622 State {
14461623 name: "restored";
@@ -1541,6 +1718,7 @@ FocusScope {
15411718 }
15421719 }
15431720 ]
1721+
15441722 transitions: [
15451723 Transition {
15461724 from: "staged,stagedWithSideStage"
@@ -1554,6 +1732,7 @@ FocusScope {
15541732 UbuntuNumberAnimation { target: appDelegate; properties: "x,y,requestedX,requestedY,requestedWidth,requestedHeight"; duration: priv.animationDuration}
15551733 },
15561734 Transition {
1735+ from: "normal,restored,maximized,maximizedHorizontally,maximizedVertically,maximizedLeft,maximizedRight,maximizedTopLeft,maximizedBottomLeft,maximizedTopRight,maximizedBottomRight,staged,stagedWithSideStage,windowedRightEdge,stagedRightEdge";
15571736 to: "spread"
15581737 // DecoratedWindow wants the scaleToPreviewSize set before enabling scaleToPreview
15591738 PropertyAction { target: appDelegate; properties: "z,visible" }
@@ -1646,7 +1825,7 @@ FocusScope {
16461825 ]
16471826
16481827 Binding {
1649− target: PanelState
1828+ target: panelState
16501829 property: "decorationsAlwaysVisible"
16511830 value: appDelegate && appDelegate.maximized && touchControls.overlayShown
16521831 }
@@ -1667,6 +1846,7 @@ FocusScope {
16671846 borderThickness: units.gu(2)
16681847 enabled: false
16691848 visible: enabled
1849+ readyToAssesBounds: !appDelegate._constructing
16701850
16711851 onPressed: {
16721852 appDelegate.activate();
@@ -1690,18 +1870,13 @@ FocusScope {
16901870 width: implicitWidth
16911871 height: implicitHeight
16921872 highlightSize: windowInfoItem.iconMargin / 2
1693− altDragEnabled: root.mode == "windowed"
16941873 boundsItem: root.availableDesktopArea
1874+ panelState: root.panelState
1875+ altDragEnabled: root.mode == "windowed"
16951876
16961877 requestedWidth: appDelegate.requestedWidth
16971878 requestedHeight: appDelegate.requestedHeight
16981879
1699− property int oldRequestedWidth: -1
1700− property int oldRequestedHeight: -1
1701−
1702− onRequestedWidthChanged: oldRequestedWidth = requestedWidth
1703− onRequestedHeightChanged: oldRequestedHeight = requestedHeight
1704−
17051880 onCloseClicked: { appDelegate.close(); }
17061881 onMaximizeClicked: {
17071882 if (appDelegate.canBeMaximized) {
@@ -1782,6 +1957,8 @@ FocusScope {
17821957 anchors.fill: decoratedWindow
17831958 enabled: false
17841959 closeable: true
1960+ stage: root
1961+ dragDelegate: fakeDragItem
17851962
17861963 onClicked: {
17871964 spreadItem.highlightedIndex = index;
@@ -1824,12 +2001,15 @@ FocusScope {
18242001 objectName: "closeMouseArea"
18252002 anchors { left: parent.left; top: parent.top; leftMargin: -height / 2; topMargin: -height / 2 + spreadMaths.closeIconOffset }
18262003 readonly property var mousePos: hoverMouseArea.mapToItem(appDelegate, hoverMouseArea.mouseX, hoverMouseArea.mouseY)
1827− visible: dragArea.distance == 0
2004+ readonly property bool shown: dragArea.distance == 0
18282005 && index == spreadItem.highlightedIndex
18292006 && mousePos.y < (decoratedWindow.height / 3)
18302007 && mousePos.y > -units.gu(4)
18312008 && mousePos.x > -units.gu(4)
18322009 && mousePos.x < (decoratedWindow.width * 2 / 3)
2010+ opacity: shown ? 1 : 0
2011+ visible: opacity > 0
2012+ Behavior on opacity { UbuntuNumberAnimation { duration: UbuntuAnimation.SnapDuration } }
18332013 height: units.gu(6)
18342014 width: height
18352015
@@ -1900,89 +2080,20 @@ FocusScope {
19002080 leftMargin: root.availableDesktopArea.x
19012081 appContainerWidth: appContainer.width
19022082 appContainerHeight: appContainer.height
2083+ panelState: root.panelState
19032084 }
19042085
1905− MouseArea {
1906− id: hoverMouseArea
1907− objectName: "hoverMouseArea"
1908− anchors.fill: appContainer
1909− propagateComposedEvents: true
1910− hoverEnabled: true
1911− enabled: false
1912− visible: enabled
1913−
1914− property int scrollAreaWidth: width / 3
1915− property bool progressiveScrollingEnabled: false
1916− property bool wasTouchPress: false
1917−
1918− onMouseXChanged: {
1919− mouse.accepted = false
1920−
1921− if (hoverMouseArea.pressed || wasTouchPress) {
1922− return;
1923− }
1924−
1925− // Find the hovered item and mark it active
1926− for (var i = appRepeater.count - 1; i >= 0; i--) {
1927− var appDelegate = appRepeater.itemAt(i);
1928− var mapped = mapToItem(appDelegate, hoverMouseArea.mouseX, hoverMouseArea.mouseY)
1929− var itemUnder = appDelegate.childAt(mapped.x, mapped.y);
1930− if (itemUnder && (itemUnder.objectName === "dragArea" || itemUnder.objectName === "windowInfoItem" || itemUnder.objectName == "closeMouseArea")) {
1931− spreadItem.highlightedIndex = i;
1932− break;
1933− }
1934− }
1935−
1936− if (floatingFlickable.contentWidth > floatingFlickable.width) {
1937− var margins = floatingFlickable.width * 0.05;
1938−
1939− if (!progressiveScrollingEnabled && mouseX < floatingFlickable.width - scrollAreaWidth) {
1940− progressiveScrollingEnabled = true
1941− }
1942−
1943− // do we need to scroll?
1944− if (mouseX < scrollAreaWidth + margins) {
1945− var progress = Math.min(1, (scrollAreaWidth + margins - mouseX) / (scrollAreaWidth - margins));
1946− var contentX = (1 - progress) * (floatingFlickable.contentWidth - floatingFlickable.width)
1947− floatingFlickable.contentX = Math.max(0, Math.min(floatingFlickable.contentX, contentX))
1948− }
1949− if (mouseX > floatingFlickable.width - scrollAreaWidth && progressiveScrollingEnabled) {
1950− var progress = Math.min(1, (mouseX - (floatingFlickable.width - scrollAreaWidth)) / (scrollAreaWidth - margins))
1951− var contentX = progress * (floatingFlickable.contentWidth - floatingFlickable.width)
1952− floatingFlickable.contentX = Math.min(floatingFlickable.contentWidth - floatingFlickable.width, Math.max(floatingFlickable.contentX, contentX))
1953− }
1954− }
1955− }
1956−
1957− onPressed: {
1958− mouse.accepted = false;
1959− wasTouchPress = mouse.source === Qt.MouseEventSynthesizedByQt;
1960− }
1961−
1962− onExited: wasTouchPress = false;
1963− }
1964−
1965− FloatingFlickable {
1966− id: floatingFlickable
1967− objectName: "spreadFlickable"
1968− anchors.fill: appContainer
1969− enabled: false
1970− contentWidth: spreadItem.spreadTotalWidth
1971−
1972− function snap(toIndex) {
1973− var delegate = appRepeater.itemAt(toIndex)
1974− var targetContentX = floatingFlickable.contentWidth / spreadItem.totalItemCount * toIndex;
1975− if (targetContentX - floatingFlickable.contentX > spreadItem.rightStackXPos - (spreadItem.spreadItemWidth / 2)) {
1976− var offset = (spreadItem.rightStackXPos - (spreadItem.spreadItemWidth / 2)) - (targetContentX - floatingFlickable.contentX)
1977− snapAnimation.to = Math.max(0, floatingFlickable.contentX - offset);
1978− snapAnimation.start();
1979− } else if (targetContentX - floatingFlickable.contentX < spreadItem.leftStackXPos + units.gu(1)) {
1980− var offset = (spreadItem.leftStackXPos + units.gu(1)) - (targetContentX - floatingFlickable.contentX);
1981− snapAnimation.to = Math.max(0, floatingFlickable.contentX - offset);
1982− snapAnimation.start();
2086+ WorkspaceSwitcher {
2087+ id: workspaceSwitcher
2088+ anchors.centerIn: parent
2089+ height: units.gu(20)
2090+ width: root.width - units.gu(8)
2091+ background: root.background
2092+ onActiveChanged: {
2093+ if (!active) {
2094+ appContainer.focus = true;
19832095 }
19842096 }
1985− UbuntuNumberAnimation {id: snapAnimation; target: floatingFlickable; property: "contentX"}
19862097 }
19872098
19882099 PropertyAnimation {
qml/Stage/WindowDecoration.qml+2
@@ -35,6 +35,7 @@ MouseArea {
3535 property bool enableMenus: true
3636 property bool windowMoving: false
3737 property alias windowControlButtonsVisible: buttons.visible
38+ property PanelState panelState
3839
3940 readonly property real buttonsWidth: buttons.width + row.spacing
4041
@@ -147,6 +148,7 @@ MouseArea {
147148 enableKeyFilter: valid && root.active && root.enableMenus
148149 unityMenuModel: root.menu
149150 windowMoving: root.windowMoving
151+ panelState: root.panelState
150152
151153 onPressed: root.onPressed(mouse)
152154 onPressedChangedEx: root.pressedChangedEx(pressed, pressedButtons, mouseX, mouseY)
qml/Stage/WindowResizeArea.qml+27−4
@@ -36,35 +36,58 @@ MouseArea {
3636 property int minWidth: 0
3737 property int minHeight: 0
3838
39+ property bool readyToAssesBounds: false
40+ onReadyToAssesBoundsChanged: d.reassesBounds()
41+
3942 QtObject {
4043 id: d
4144
4245 readonly property int maxSafeInt: 2147483647
4346 readonly property int maxSizeIncrement: units.gu(40)
4447
48+ function reassesBounds() {
49+ if (!readyToAssesBounds) return;
50+
51+ if (target.windowedWidth < minimumWidth) {
52+ target.windowedWidth = minimumWidth;
53+ }
54+ if (target.windowedHeight < minimumHeight) {
55+ target.windowedHeight = minimumHeight;
56+ }
57+ if (target.windowedHeight < minimumHeight) {
58+ target.windowedHeight = minimumHeight;
59+ }
60+ if (target.windowedWidth > maximumWidth) {
61+ target.windowedWidth = maximumWidth;
62+ }
63+ if (target.windowedHeight > maximumHeight) {
64+ target.windowedHeight = maximumHeight;
65+ }
66+ }
67+
4568 readonly property int minimumWidth: root.target ? Math.max(root.minWidth, root.target.minimumWidth) : root.minWidth
4669 onMinimumWidthChanged: {
47− if (target.windowedWidth < minimumWidth) {
70+ if (readyToAssesBounds && target.windowedWidth < minimumWidth) {
4871 target.windowedWidth = minimumWidth;
4972 }
5073 }
5174 readonly property int minimumHeight: root.target ? Math.max(root.minHeight, root.target.minimumHeight) : root.minHeight
5275 onMinimumHeightChanged: {
53− if (target.windowedHeight < minimumHeight) {
76+ if (readyToAssesBounds && target.windowedHeight < minimumHeight) {
5477 target.windowedHeight = minimumHeight;
5578 }
5679 }
5780 readonly property int maximumWidth: root.target && root.target.maximumWidth >= minimumWidth && root.target.maximumWidth > 0
5881 ? root.target.maximumWidth : maxSafeInt
5982 onMaximumWidthChanged: {
60− if (target.windowedWidth > maximumWidth) {
83+ if (readyToAssesBounds && target.windowedWidth > maximumWidth) {
6184 target.windowedWidth = maximumWidth;
6285 }
6386 }
6487 readonly property int maximumHeight: root.target && root.target.maximumHeight >= minimumHeight && root.target.maximumHeight > 0
6588 ? root.target.maximumHeight : maxSafeInt
6689 onMaximumHeightChanged: {
67− if (target.windowedHeight > maximumHeight) {
90+ if (readyToAssesBounds && target.windowedHeight > maximumHeight) {
6891 target.windowedHeight = maximumHeight;
6992 }
7093 }
qml/Stage/WindowStateSaver.qml+4−1
@@ -43,7 +43,10 @@ QtObject {
4343 (target.fullscreen ? 0 : root.leftMargin)); });
4444 target.windowedY = Qt.binding(function() { return Math.max(Math.min(windowGeometry.y, screenHeight - target.windowedHeight), minimumY); });
4545
46− target.updateNormalGeometry();
46+ target.normalWidth = target.windowedWidth;
47+ target.normalHeight = target.windowedHeight;
48+ target.normalX = target.windowedX;
49+ target.normalY = target.windowedY;
4750
4851 // initialize the x/y to restore to
4952 target.restoredX = target.normalX;
qml/Stage/WorkspaceSwitcher.qmlnew file+196
@@ -0,0 +1,196 @@
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 Ubuntu.Components 1.3
19+import "Spread"
20+import WindowManager 1.0
21+import Unity.Application 0.1
22+
23+Item {
24+ id: root
25+
26+ opacity: d.shown ? 1 : 0
27+ visible: opacity > 0
28+ Behavior on opacity { UbuntuNumberAnimation {} }
29+
30+ property var screensProxy: Screens.createProxy();
31+ property string background
32+
33+ readonly property alias active: d.active
34+
35+ function showLeft() {
36+ show();
37+ d.previousWorkspace();
38+ }
39+ function showRight() {
40+ show();
41+ d.nextWorkspace();
42+ }
43+ function showUp() {
44+ show();
45+ d.previousScreen();
46+ }
47+ function showDown() {
48+ show();
49+ d.nextScreen();
50+ }
51+
52+ function show() {
53+ hideTimer.stop();
54+ d.altPressed = true;
55+ d.ctrlPressed = true;
56+ d.active = true;
57+ d.shown = true;
58+ focus = true;
59+
60+ d.highlightedScreenIndex = screensProxy.activeScreen;
61+ var activeScreen = screensProxy.get(screensProxy.activeScreen);
62+ d.highlightedWorkspaceIndex = activeScreen.workspaces.indexOf(activeScreen.currentWorkspace)
63+ }
64+
65+ QtObject {
66+ id: d
67+
68+ property bool active: false
69+ property bool shown: false
70+ property bool altPressed: false
71+ property bool ctrlPressed: false
72+
73+ property int rowHeight: root.height - units.gu(4)
74+
75+ property int highlightedScreenIndex: -1
76+ property int highlightedWorkspaceIndex: -1
77+
78+ function previousWorkspace() {
79+ highlightedWorkspaceIndex = Math.max(highlightedWorkspaceIndex - 1, 0);
80+ }
81+ function nextWorkspace() {
82+ var screen = screensProxy.get(highlightedScreenIndex);
83+ highlightedWorkspaceIndex = Math.min(highlightedWorkspaceIndex + 1, screen.workspaces.count - 1);
84+ }
85+ function previousScreen() {
86+ highlightedScreenIndex = Math.max(highlightedScreenIndex - 1, 0);
87+ var screen = screensProxy.get(highlightedScreenIndex);
88+ highlightedWorkspaceIndex = Math.min(highlightedWorkspaceIndex, screen.workspaces.count - 1)
89+ }
90+ function nextScreen() {
91+ highlightedScreenIndex = Math.min(highlightedScreenIndex + 1, screensProxy.count - 1);
92+ var screen = screensProxy.get(highlightedScreenIndex);
93+ highlightedWorkspaceIndex = Math.min(highlightedWorkspaceIndex, screen.workspaces.count - 1)
94+ }
95+ }
96+
97+ Timer {
98+ id: hideTimer
99+ interval: 300
100+ onTriggered: d.shown = false;
101+ }
102+
103+ Keys.onPressed: {
104+ switch (event.key) {
105+ case Qt.Key_Left:
106+ d.previousWorkspace();
107+ break;
108+ case Qt.Key_Right:
109+ d.nextWorkspace()
110+ break;
111+ case Qt.Key_Up:
112+ d.previousScreen();
113+ break;
114+ case Qt.Key_Down:
115+ d.nextScreen();
116+ }
117+ }
118+ Keys.onReleased: {
119+ switch (event.key) {
120+ case Qt.Key_Alt:
121+ d.altPressed = false;
122+ break;
123+ case Qt.Key_Control:
124+ d.ctrlPressed = false;
125+ break;
126+ }
127+
128+ if (!d.altPressed && !d.ctrlPressed) {
129+ d.active = false;
130+ hideTimer.start();
131+ focus = false;
132+ screensProxy.get(d.highlightedScreenIndex).workspaces.get(d.highlightedWorkspaceIndex).activate();
133+ }
134+ }
135+
136+ UbuntuShape {
137+ backgroundColor: "#F2111111"
138+ clip: true
139+ width: Math.min(parent.width, screensColumn.width + units.gu(4))
140+ anchors.horizontalCenter: parent.horizontalCenter
141+ height: parent.height
142+
143+ Column {
144+ id: screensColumn
145+ anchors {
146+ top: parent.top; topMargin: units.gu(2) - d.highlightedScreenIndex * (d.rowHeight + screensColumn.spacing)
147+ left: parent.left; leftMargin: units.gu(2)
148+ }
149+ width: screensRepeater.itemAt(d.highlightedScreenIndex).width
150+ spacing: units.gu(2)
151+ Behavior on anchors.topMargin { UbuntuNumberAnimation {} }
152+ Behavior on width { UbuntuNumberAnimation {} }
153+
154+ Repeater {
155+ id: screensRepeater
156+ model: screensProxy
157+
158+ delegate: Item {
159+ height: d.rowHeight
160+ width: workspaces.width
161+ anchors.horizontalCenter: parent.horizontalCenter
162+ opacity: d.highlightedScreenIndex == index ? 1 : 0
163+ Behavior on opacity { UbuntuNumberAnimation {} }
164+
165+ UbuntuShape {
166+ id: header
167+ anchors { left: parent.left; top: parent.top; right: parent.right }
168+ height: units.gu(4)
169+ backgroundColor: "white"
170+
171+ Label {
172+ anchors { left: parent.left; top: parent.top; right: parent.right; margins: units.gu(1) }
173+ text: model.screen.name
174+ color: UbuntuColors.ash
175+ }
176+ }
177+
178+ Workspaces {
179+ id: workspaces
180+ height: parent.height - header.height - units.gu(2)
181+ width: Math.min(implicitWidth, root.width - units.gu(4))
182+
183+ anchors.bottom: parent.bottom
184+ anchors.bottomMargin: units.gu(1)
185+ anchors.horizontalCenter: parent.horizontalCenter
186+ screen: model.screen
187+ background: root.background
188+ selectedIndex: d.highlightedScreenIndex == index ? d.highlightedWorkspaceIndex : -1
189+
190+ workspaceModel: model.screen.workspaces
191+ }
192+ }
193+ }
194+ }
195+ }
196+}
qml/Stage/graphics/multi-monitor_drop-here.pngnew file

Binary file not shown.

qml/Stage/graphics/multi-monitor_leave.pngnew file

Binary file not shown.

qml/qmldirnew file+1
@@ -0,0 +1 @@
1+singleton ShellNotifier 0.1 ShellNotifier.qml
src/ApplicationArguments.cpp+11−2
@@ -16,7 +16,16 @@
1616
1717#include "ApplicationArguments.h"
1818
19−ApplicationArguments::ApplicationArguments(QObject *parent)
20− : QObject(parent)
19+ApplicationArguments::ApplicationArguments(QCoreApplication *app)
20+ : QObject(app)
21+ , UnityCommandLineParser(*app)
2122{
2223}
24+
25+void ApplicationArguments::setDeviceName(const QString &deviceName)
26+{
27+ if (deviceName != m_deviceName) {
28+ m_deviceName = deviceName;
29+ Q_EMIT deviceNameChanged(m_deviceName);
30+ }
31+}
src/ApplicationArguments.h+17−15
@@ -23,31 +23,33 @@
2323#include <QSize>
2424#include <QString>
2525
26−class ApplicationArguments : public QObject
26+#include "UnityCommandLineParser.h"
27+
28+class ApplicationArguments : public QObject,
29+ public UnityCommandLineParser
2730{
2831 Q_OBJECT
2932 Q_PROPERTY(QString deviceName READ deviceName NOTIFY deviceNameChanged)
3033 Q_PROPERTY(QString mode READ mode CONSTANT)
34+
35+ Q_PROPERTY(bool hasGeometry READ hasGeometry CONSTANT)
36+ Q_PROPERTY(QSize windowGeometry READ windowGeometry CONSTANT)
37+ Q_PROPERTY(bool hasTestability READ hasTestability CONSTANT)
38+ Q_PROPERTY(bool hasFrameless READ hasFrameless CONSTANT)
39+ Q_PROPERTY(bool hasFullscreen READ hasFullscreen CONSTANT)
40+#ifdef UNITY8_ENABLE_TOUCH_EMULATION
41+ Q_PROPERTY(bool hasMouseToTouch READ hasMouseToTouch CONSTANT)
42+#endif
43+
3144public:
32− ApplicationArguments(QObject *parent = nullptr);
45+ ApplicationArguments(QCoreApplication *app);
3346
34− void setDeviceName(const QString &deviceName) {
35− if (deviceName != m_deviceName) {
36− m_deviceName = deviceName;
37− Q_EMIT deviceNameChanged(m_deviceName);
38− }
39− }
40− QString deviceName() const { return m_deviceName; }
47+ void setDeviceName(const QString &deviceName);
4148
42− void setMode(const QString &mode) { m_mode = mode; }
43− QString mode() const { return m_mode; }
49+ bool hasGeometry() const { return m_windowGeometry.isValid(); }
4450
4551Q_SIGNALS:
4652 void deviceNameChanged(const QString&);
47−
48−private:
49− QString m_deviceName;
50− QString m_mode;
5153};
5254
5355#endif // APPLICATION_ARGUMENTS_H
src/CMakeLists.txt+12−4
@@ -9,6 +9,7 @@ include_directories(
99 ${Qt5Quick_PRIVATE_INCLUDE_DIRS}
1010 ${GSETTINGS_QT_INCLUDE_DIRS}
1111 ${CONNECTIVITY_INCLUDE_DIRS}
12+ ${QTMIRSERVER_INCLUDE_DIRS}
1213)
1314
1415include_directories(
@@ -29,12 +30,12 @@ set(SOURCE_FILES
2930 ApplicationArguments.cpp
3031 main.cpp
3132 CachingNetworkManagerFactory.cpp
32− SecondaryWindow.cpp
33− ShellApplication.cpp
34− ShellView.cpp
33+ DisplayConfigurationStorage.cpp
34+ UnityApplication.cpp
3535 UnityCommandLineParser.cpp
3636 UnixSignalHandler.cpp
3737 DebuggingController.cpp
38+ WindowManagementPolicy.cpp
3839 ${QML_FILES} # This is to make qml and image files appear in the IDE's project tree
3940)
4041
@@ -49,7 +50,14 @@ qt5_use_modules(${SHELL_APP} DBus Gui Qml Quick Test)
4950if (NOT "${ANDROID_PROPERTIES_INCLUDE_DIRS}" STREQUAL "")
5051 set_target_properties(${SHELL_APP} PROPERTIES INCLUDE_DIRECTORIES ${ANDROID_PROPERTIES_INCLUDE_DIRS})
5152endif()
52−target_link_libraries(${SHELL_APP} ${ANDROID_PROPERTIES_LDFLAGS} ${GSETTINGS_QT_LDFLAGS} UbuntuGestures connectivity-qt1 unity8-private)
53+target_link_libraries(${SHELL_APP}
54+ ${ANDROID_PROPERTIES_LDFLAGS}
55+ ${GSETTINGS_QT_LDFLAGS}
56+ ${QTMIRSERVER_LDFLAGS}
57+ UbuntuGestures
58+ connectivity-qt1
59+ unity8-private
60+)
5361
5462if (ENABLE_TOUCH_EMULATION)
5563 target_link_libraries(${SHELL_APP} ${MOUSETOUCHADAPTOR_LIBS_LDFLAGS})
src/DisplayConfigurationStorage.cppnew file+90
@@ -0,0 +1,90 @@
1+#include "DisplayConfigurationStorage.h"
2+
3+#include <QFile>
4+#include <QStandardPaths>
5+#include <QJsonObject>
6+#include <QJsonDocument>
7+
8+namespace {
9+
10+inline QString stringFromEdid(const miral::Edid& edid)
11+{
12+ QString str;
13+ str += QString::fromStdString(edid.vendor);
14+ str += QString("%1%2").arg(edid.product_code).arg(edid.serial_number);
15+
16+ for (int i = 0; i < 4; i++) {
17+ str += QString::fromStdString(edid.descriptors[i].string_value());
18+ }
19+ return str;
20+}
21+
22+}
23+
24+DisplayConfigurationStorage::DisplayConfigurationStorage()
25+{
26+}
27+
28+void DisplayConfigurationStorage::save(const miral::DisplayId &displayId, const miral::DisplayConfigurationOptions &options)
29+{
30+ const QString dbPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QStringLiteral("/unity8/");
31+ QFile f(dbPath + stringFromEdid(displayId.edid) + ".edid");
32+
33+ QJsonObject json;
34+ if (options.used.is_set()) json.insert("used", options.used.value());
35+ if (options.clone_output_index.is_set()) json.insert("clone_output_index", static_cast<int>(options.clone_output_index.value()));
36+ if (options.mode.is_set()) {
37+ auto const& mode = options.mode.value();
38+
39+ QString sz(QString("%1x%2").arg(mode.size.width.as_int()).arg(mode.size.height.as_int()));
40+ QJsonObject jsonMode({
41+ {"size", sz},
42+ {"refresh_rate", mode.refresh_rate }
43+ });
44+ json.insert("mode", jsonMode);
45+ }
46+ if (options.orientation.is_set()) json.insert("orientation", static_cast<int>(options.orientation.value()));
47+ if (options.form_factor.is_set()) json.insert("form_factor", static_cast<int>(options.form_factor.value()));
48+ if (options.scale.is_set()) json.insert("scale", options.scale.value());
49+
50+ if (f.open(QIODevice::WriteOnly)) {
51+ QJsonDocument saveDoc(json);
52+ f.write(saveDoc.toJson());
53+ }
54+}
55+
56+bool DisplayConfigurationStorage::load(const miral::DisplayId &displayId, miral::DisplayConfigurationOptions &options) const
57+{
58+ const QString dbPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QStringLiteral("/unity8/");
59+ QFile f(dbPath + stringFromEdid(displayId.edid) + ".edid");
60+
61+ if (f.open(QIODevice::ReadOnly)) {
62+ QByteArray saveData = f.readAll();
63+ QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
64+
65+ QJsonObject json(loadDoc.object());
66+ if (json.contains("used")) options.used = json["used"].toBool();
67+ if (json.contains("clone_output_index")) options.clone_output_index = json["clone_output_index"].toInt();
68+ if (json.contains("mode")) {
69+ QJsonObject jsonMode = json["mode"].toObject();
70+
71+ if (jsonMode.contains("size") && jsonMode.contains("refresh_rate")) {
72+ QString sz(jsonMode["size"].toString());
73+ QStringList geo = sz.split("x", QString::SkipEmptyParts);
74+ if (geo.count() == 2) {
75+ miral::DisplayConfigurationOptions::DisplayMode mode;
76+ mode.size = mir::geometry::Size(geo[0].toInt(), geo[1].toInt());
77+ mode.refresh_rate = jsonMode["refresh_rate"].toDouble();
78+ options.mode = mode;
79+ }
80+ }
81+ }
82+ if (json.contains("orientation")) options.orientation = static_cast<MirOrientation>(json["orientation"].toInt());
83+ if (json.contains("form_factor")) options.form_factor = static_cast<MirFormFactor>(json["form_factor"].toInt());
84+ if (json.contains("scale")) options.scale = json["form_factor"].toDouble();
85+
86+ return true;
87+ }
88+
89+ return false;
90+}
src/DisplayConfigurationStorage.hnew file+15
@@ -0,0 +1,15 @@
1+#ifndef UNITY_DISPLAYCONFIGURATIONSTORAGE_H
2+#define UNITY_DISPLAYCONFIGURATIONSTORAGE_H
3+
4+#include <qtmir/miral/display_configuration_storage.h>
5+
6+class DisplayConfigurationStorage : public miral::DisplayConfigurationStorage
7+{
8+public:
9+ DisplayConfigurationStorage();
10+
11+ void save(const miral::DisplayId& displayId, const miral::DisplayConfigurationOptions& options) override;
12+ bool load(const miral::DisplayId& displayId, miral::DisplayConfigurationOptions& options) const override;
13+};
14+
15+#endif // UNITY_DISPLAYCONFIGURATIONSTORAGE_H
src/SecondaryWindow.cppdeleted−40
@@ -1,40 +0,0 @@
1−/*
2− * Copyright (C) 2015 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#include "SecondaryWindow.h"
18−
19−// local
20−#include <paths.h>
21−
22−#include <QQmlContext>
23−
24−SecondaryWindow::SecondaryWindow(QQmlEngine *engine)
25− : QQuickView(engine, nullptr)
26−{
27− QByteArray pxpguEnv = qgetenv("GRID_UNIT_PX");
28− bool ok;
29− int pxpgu = pxpguEnv.toInt(&ok);
30− if (!ok) {
31− pxpgu = 8;
32− }
33− engine->rootContext()->setContextProperty(QStringLiteral("internalGu"), pxpgu);
34− setResizeMode(QQuickView::SizeRootObjectToView);
35− setColor("black");
36− setTitle(QStringLiteral("Unity8 Shell - Secondary Screen"));
37−
38− QUrl source(::qmlDirectory() + "/DisabledScreenNotice.qml");
39− setSource(source);
40−}
src/ShellApplication.cppdeleted−221
@@ -1,221 +0,0 @@
1−/*
2− * Copyright (C) 2015 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#include "ShellApplication.h"
18−
19−// Qt
20−#include <QLibrary>
21−#include <QProcess>
22−#include <QScreen>
23−
24−#include <QGSettings>
25−
26−#include <libintl.h>
27−
28−// libandroid-properties
29−#include <hybris/properties/properties.h>
30−
31−// local
32−#include <paths.h>
33−#include "CachingNetworkManagerFactory.h"
34−#include "UnityCommandLineParser.h"
35−#include "DebuggingController.h"
36−
37−ShellApplication::ShellApplication(int & argc, char ** argv, bool isMirServer)
38− : QGuiApplication(argc, argv)
39−{
40− setApplicationName(QStringLiteral("unity8"));
41− setOrganizationName(QStringLiteral("Canonical"));
42−
43− connect(this, &QGuiApplication::screenAdded, this, &ShellApplication::onScreenAdded);
44−
45− setupQmlEngine(isMirServer);
46−
47− UnityCommandLineParser parser(*this);
48−
49− if (!parser.deviceName().isEmpty()) {
50− m_deviceName = parser.deviceName();
51− } else {
52− char buffer[200];
53− property_get("ro.product.device", buffer /* value */, "desktop" /* default_value*/);
54− m_deviceName = QString(buffer);
55− }
56− m_qmlArgs.setDeviceName(m_deviceName);
57−
58− m_qmlArgs.setMode(parser.mode());
59−
60− // The testability driver is only loaded by QApplication but not by QGuiApplication.
61− // However, QApplication depends on QWidget which would add some unneeded overhead => Let's load the testability driver on our own.
62− if (parser.hasTestability() || getenv("QT_LOAD_TESTABILITY")) {
63− QLibrary testLib(QStringLiteral("qttestability"));
64− if (testLib.load()) {
65− typedef void (*TasInitialize)(void);
66− TasInitialize initFunction = (TasInitialize)testLib.resolve("qt_testability_init");
67− if (initFunction) {
68− initFunction();
69− } else {
70− qCritical("Library qttestability resolve failed!");
71− }
72− } else {
73− qCritical("Library qttestability load failed!");
74− }
75− }
76−
77− bindtextdomain("unity8", translationDirectory().toUtf8().data());
78− textdomain("unity8");
79−
80− QScopedPointer<QGSettings> gSettings(new QGSettings("com.canonical.Unity8"));
81− gSettings->reset(QStringLiteral("alwaysShowOsk"));
82−
83− m_shellView = new ShellView(m_qmlEngine, &m_qmlArgs);
84−
85− if (parser.windowGeometry().isValid()) {
86− m_shellView->setWidth(parser.windowGeometry().width());
87− m_shellView->setHeight(parser.windowGeometry().height());
88− }
89−
90− if (parser.hasFrameless()) {
91− m_shellView->setFlags(Qt::FramelessWindowHint);
92− }
93−
94−
95− #ifdef UNITY8_ENABLE_TOUCH_EMULATION
96− // You will need this if you want to interact with touch-only components using a mouse
97− // Needed only when manually testing on a desktop.
98− if (parser.hasMouseToTouch()) {
99− m_mouseTouchAdaptor = MouseTouchAdaptor::instance();
100− }
101− #endif
102−
103− new DebuggingController(this);
104−
105− // Some hard-coded policy for now.
106− // NB: We don't support more than two screens at the moment
107− //
108− // TODO: Support an arbitrary number of screens and different policies
109− // (eg cloned desktop, several desktops, etc)
110− if (isMirServer && screens().count() == 2) {
111− m_shellView->setScreen(screens().at(1));
112− m_qmlArgs.setDeviceName(QStringLiteral("desktop"));
113−
114− m_secondaryWindow = new SecondaryWindow(m_qmlEngine);
115− m_secondaryWindow->setScreen(screens().at(0));
116− // QWindow::showFullScreen() also calls QWindow::requestActivate() and we don't want that!
117− m_secondaryWindow->setWindowState(Qt::WindowFullScreen);
118− m_secondaryWindow->setVisible(true);
119− }
120−
121− if (parser.mode().compare("greeter") == 0) {
122− QSize primaryScreenSize = this->primaryScreen()->size();
123− m_shellView->setHeight(primaryScreenSize.height());
124− m_shellView->setWidth(primaryScreenSize.width());
125− m_shellView->show();
126− m_shellView->requestActivate();
127− if (!QProcess::startDetached("initctl emit --no-wait unity8-greeter-started")) {
128− qDebug() << "Unable to send unity8-greeter-started event to Upstart";
129− }
130− } else if (isMirServer || parser.hasFullscreen()) {
131− m_shellView->showFullScreen();
132− } else {
133− m_shellView->show();
134− }
135−}
136−
137−ShellApplication::~ShellApplication()
138−{
139− destroyResources();
140−}
141−
142−void ShellApplication::destroyResources()
143−{
144− // Deletion order is important. Don't use QScopedPointers and the like
145− // Otherwise the process will hang on shutdown (bug somewhere I guess).
146− delete m_shellView;
147− m_shellView = nullptr;
148−
149− delete m_secondaryWindow;
150− m_secondaryWindow = nullptr;
151−
152− #ifdef UNITY8_ENABLE_TOUCH_EMULATION
153− delete m_mouseTouchAdaptor;
154− m_mouseTouchAdaptor = nullptr;
155− #endif
156−
157− delete m_qmlEngine;
158− m_qmlEngine = nullptr;
159−}
160−
161−void ShellApplication::setupQmlEngine(bool isMirServer)
162−{
163− m_qmlEngine = new QQmlEngine(this);
164−
165− m_qmlEngine->setBaseUrl(QUrl::fromLocalFile(::qmlDirectory()));
166−
167− prependImportPaths(m_qmlEngine, ::overrideImportPaths());
168− if (!isMirServer) {
169− prependImportPaths(m_qmlEngine, ::nonMirImportPaths());
170− }
171− appendImportPaths(m_qmlEngine, ::fallbackImportPaths());
172−
173− m_qmlEngine->setNetworkAccessManagerFactory(new CachingNetworkManagerFactory);
174−
175− QObject::connect(m_qmlEngine, &QQmlEngine::quit, this, &QGuiApplication::quit);
176−}
177−
178−void ShellApplication::onScreenAdded(QScreen * /*screen*/)
179−{
180− // TODO: Support an arbitrary number of screens and different policies
181− // (eg cloned desktop, several desktops, etc)
182− if (screens().count() == 2) {
183− m_shellView->setScreen(screens().at(1));
184− m_qmlArgs.setDeviceName(QStringLiteral("desktop"));
185− // Changing the QScreen where a QWindow is drawn makes it also lose focus (besides having
186− // its backing QPlatformWindow recreated). So lets refocus it.
187− m_shellView->requestActivate();
188− // QWindow::destroy() is called when it changes between screens. We have to manually make it visible again
189− // <dandrader> This bug is supposedly fixed in Qt 5.5.1, although I can still reproduce it there. :-/
190− m_shellView->setVisible(true);
191−
192− m_secondaryWindow = new SecondaryWindow(m_qmlEngine);
193− m_secondaryWindow->setScreen(screens().at(0));
194−
195− // QWindow::showFullScreen() also calls QWindow::requestActivate() and we don't want that!
196− m_secondaryWindow->setWindowState(Qt::WindowFullScreen);
197− m_secondaryWindow->setVisible(true);
198− }
199−}
200−
201−void ShellApplication::onScreenAboutToBeRemoved(QScreen *screen)
202−{
203− // TODO: Support an arbitrary number of screens and different policies
204− // (eg cloned desktop, several desktops, etc)
205− if (screen == m_shellView->screen()) {
206− const QList<QScreen *> allScreens = screens();
207− Q_ASSERT(allScreens.count() > 1);
208− Q_ASSERT(allScreens.at(0) != screen);
209− Q_ASSERT(m_secondaryWindow);
210− delete m_secondaryWindow;
211− m_secondaryWindow = nullptr;
212− m_shellView->setScreen(allScreens.first());
213− m_qmlArgs.setDeviceName(m_deviceName);
214− // Changing the QScreen where a QWindow is drawn makes it also lose focus (besides having
215− // its backing QPlatformWindow recreated). So lets refocus it.
216− m_shellView->requestActivate();
217− // QWindow::destroy() is called when it changes between screens. We have to manually make it visible again
218− // <dandrader> This bug is supposedly fixed in Qt 5.5.1, although I can still reproduce it there. :-/
219− m_shellView->setVisible(true);
220− }
221−}
src/ShellView.cppdeleted−85
@@ -1,85 +0,0 @@
1−/*
2− * Copyright (C) 2015 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#include "ShellView.h"
18−
19−// Qt
20−#include <QQmlContext>
21−#include <QQuickItem>
22−#include <QtQuick/private/qquickitem_p.h>
23−#include <QtQuick/private/qquickrectangle_p.h>
24−#include <QtQuick/private/qquicktext_p.h>
25−
26−// local
27−#include <paths.h>
28−
29−ShellView::ShellView(QQmlEngine *engine, QObject *qmlArgs)
30− : QQuickView(engine, nullptr)
31−{
32− setResizeMode(QQuickView::SizeRootObjectToView);
33− setColor("black");
34− setTitle(QStringLiteral("Unity8"));
35−
36− rootContext()->setContextProperty(QStringLiteral("applicationArguments"), qmlArgs);
37−
38− connect(this, &QQuickView::statusChanged, this, [this] {
39− if (status() == QQuickView::Error) {
40− QQuickRectangle *rect = new QQuickRectangle(contentItem());
41− rect->setColor(Qt::white);
42− QQuickItemPrivate::get(rect)->anchors()->setFill(contentItem());
43−
44− QString errorsString;
45− for(const QQmlError &e: errors()) {
46− errorsString += e.toString() + "\n";
47− }
48− QQuickText *text = new QQuickText(rect);
49− text->setColor(Qt::black);
50− text->setWrapMode(QQuickText::Wrap);
51− text->setText(QString("There was an error loading Unity8:\n%1").arg(errorsString));
52− QQuickItemPrivate::get(text)->anchors()->setFill(rect);
53− }
54− }
55− );
56−
57− QUrl source(::qmlDirectory() + "/OrientedShell.qml");
58− setSource(source);
59−
60− connect(this, &QWindow::widthChanged, this, &ShellView::onWidthChanged);
61− connect(this, &QWindow::heightChanged, this, &ShellView::onHeightChanged);
62−}
63−
64−void ShellView::onWidthChanged(int w)
65−{
66− // For good measure in case SizeRootObjectToView doesn't fulfill its promise.
67− //
68− // There's at least one situation that's know to leave the root object with an outdated size.
69− // (really looks like Qt bug)
70− // Happens when starting unity8 with an external monitor already connected.
71− // The QResizeEvent we get still has the size of the first screen and since the resize move is triggered
72− // from the resize event handler, the root item doesn't get resized.
73− // TODO: Confirm the Qt bug and submit a patch upstream
74− if (rootObject()) {
75− rootObject()->setWidth(w);
76− }
77−}
78−
79−void ShellView::onHeightChanged(int h)
80−{
81− // See comment in ShellView::onWidthChanged()
82− if (rootObject()) {
83− rootObject()->setHeight(h);
84− }
85−}
src/UnityApplication.cppnew file+152
@@ -0,0 +1,152 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "UnityApplication.h"
18+
19+// Qt
20+#include <QLibrary>
21+#include <QProcess>
22+#include <QScreen>
23+#include <QQmlContext>
24+#include <QQmlComponent>
25+
26+#include <QGSettings>
27+
28+#include <libintl.h>
29+
30+// libandroid-properties
31+#include <hybris/properties/properties.h>
32+
33+// qtmir
34+#include <qtmir/displayconfigurationstorage.h>
35+
36+// local
37+#include <paths.h>
38+#include "CachingNetworkManagerFactory.h"
39+#include "UnityCommandLineParser.h"
40+#include "DebuggingController.h"
41+#include "WindowManagementPolicy.h"
42+#include "DisplayConfigurationStorage.h"
43+
44+#include <QDebug>
45+
46+
47+
48+UnityApplication::UnityApplication(int & argc, char ** argv)
49+ : qtmir::MirServerApplication(argc, argv, { qtmir::SetWindowManagementPolicy<WindowManagementPolicy>(),
50+ qtmir::SetDisplayConfigurationStorage<DisplayConfigurationStorage>() })
51+ , m_qmlArgs(this)
52+{
53+ setApplicationName(QStringLiteral("unity8"));
54+ setOrganizationName(QStringLiteral("Canonical"));
55+
56+ setupQmlEngine();
57+
58+ if (m_qmlArgs.deviceName().isEmpty()) {
59+ char buffer[200];
60+ property_get("ro.product.device", buffer /* value */, "desktop" /* default_value*/);
61+ m_qmlArgs.setDeviceName(QString(buffer));
62+ }
63+
64+ // The testability driver is only loaded by QApplication but not by QGuiApplication.
65+ // However, QApplication depends on QWidget which would add some unneeded overhead => Let's load the testability driver on our own.
66+ if (m_qmlArgs.hasTestability() || getenv("QT_LOAD_TESTABILITY")) {
67+ QLibrary testLib(QStringLiteral("qttestability"));
68+ if (testLib.load()) {
69+ typedef void (*TasInitialize)(void);
70+ TasInitialize initFunction = (TasInitialize)testLib.resolve("qt_testability_init");
71+ if (initFunction) {
72+ initFunction();
73+ } else {
74+ qCritical("Library qttestability resolve failed!");
75+ }
76+ } else {
77+ qCritical("Library qttestability load failed!");
78+ }
79+ }
80+
81+ bindtextdomain("unity8", translationDirectory().toUtf8().data());
82+ textdomain("unity8");
83+
84+ QScopedPointer<QGSettings> gSettings(new QGSettings("com.canonical.Unity8"));
85+ gSettings->reset(QStringLiteral("alwaysShowOsk"));
86+
87+
88+ QByteArray pxpguEnv = qgetenv("GRID_UNIT_PX");
89+ bool ok;
90+ int pxpgu = pxpguEnv.toInt(&ok);
91+ if (!ok) {
92+ pxpgu = 8;
93+ }
94+ m_qmlEngine->rootContext()->setContextProperty("internalGu", pxpgu);
95+ m_qmlEngine->rootContext()->setContextProperty(QStringLiteral("applicationArguments"), &m_qmlArgs);
96+ m_qmlEngine->rootContext()->setContextProperty("DebuggingController", new DebuggingController(this));
97+
98+ auto component(new QQmlComponent(m_qmlEngine, m_qmlArgs.qmlfie()));
99+ component->create();
100+ if (component->status() == QQmlComponent::Error) {
101+ m_qmlEngine->rootContext()->setContextProperty(QStringLiteral("errorString"), component->errorString());
102+ auto errorComponent(new QQmlComponent(m_qmlEngine,
103+ QUrl::fromLocalFile(::qmlDirectory() + "/ErrorApplication.qml")));
104+ errorComponent->create();
105+ qDebug() << errorComponent->errorString();
106+ return;
107+ }
108+
109+ #ifdef UNITY8_ENABLE_TOUCH_EMULATION
110+ // You will need this if you want to interact with touch-only components using a mouse
111+ // Needed only when manually testing on a desktop.
112+ if (m_qmlArgs.hasMouseToTouch()) {
113+ m_mouseTouchAdaptor = MouseTouchAdaptor::instance();
114+ }
115+ #endif
116+
117+ if (m_qmlArgs.mode().compare("greeter") == 0) {
118+ if (!QProcess::startDetached("initctl emit --no-wait unity8-greeter-started")) {
119+ qDebug() << "Unable to send unity8-greeter-started event to Upstart";
120+ }
121+ }
122+}
123+
124+UnityApplication::~UnityApplication()
125+{
126+ destroyResources();
127+}
128+
129+void UnityApplication::destroyResources()
130+{
131+ #ifdef UNITY8_ENABLE_TOUCH_EMULATION
132+ delete m_mouseTouchAdaptor;
133+ m_mouseTouchAdaptor = nullptr;
134+ #endif
135+
136+ delete m_qmlEngine;
137+ m_qmlEngine = nullptr;
138+}
139+
140+void UnityApplication::setupQmlEngine()
141+{
142+ m_qmlEngine = new QQmlEngine(this);
143+
144+ m_qmlEngine->setBaseUrl(QUrl::fromLocalFile(::qmlDirectory()));
145+
146+ prependImportPaths(m_qmlEngine, ::overrideImportPaths());
147+ appendImportPaths(m_qmlEngine, ::fallbackImportPaths());
148+
149+ m_qmlEngine->setNetworkAccessManagerFactory(new CachingNetworkManagerFactory);
150+
151+ QObject::connect(m_qmlEngine, &QQmlEngine::quit, this, &QGuiApplication::quit);
152+}
src/ShellApplication.h → src/UnityApplication.hrenamed+9−19
@@ -14,11 +14,11 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17−#ifndef SHELLAPPLICATION_H
18−#define SHELLAPPLICATION_H
17+#ifndef UNITYAPPLICATION_H
18+#define UNITYAPPLICATION_H
1919
2020#include <QGuiApplication>
21−#include <QQmlEngine>
21+#include <QQmlApplicationEngine>
2222#include <QQuickView>
2323#include <QScopedPointer>
2424
@@ -28,30 +28,20 @@
2828#include "MouseTouchAdaptor.h"
2929#endif
3030
31−#include "SecondaryWindow.h"
32−#include "ShellView.h"
31+#include <qtmir/mirserverapplication.h>
3332
34−class ShellApplication : public QGuiApplication
33+class UnityApplication : public qtmir::MirServerApplication
3534{
3635 Q_OBJECT
3736public:
38− ShellApplication(int & argc, char ** argv, bool isMirServer);
39− virtual ~ShellApplication();
37+ UnityApplication(int & argc, char ** argv);
38+ virtual ~UnityApplication();
4039
4140 void destroyResources();
42−public Q_SLOTS:
43− // called by qtmir
44− void onScreenAboutToBeRemoved(QScreen *screen);
45−
46−private Q_SLOTS:
47− void onScreenAdded(QScreen*);
4841
4942private:
50− void setupQmlEngine(bool isMirServer);
51− QString m_deviceName;
43+ void setupQmlEngine();
5244 ApplicationArguments m_qmlArgs;
53− ShellView *m_shellView{nullptr};
54− SecondaryWindow *m_secondaryWindow{nullptr};
5545
5646 #ifdef UNITY8_ENABLE_TOUCH_EMULATION
5747 MouseTouchAdaptor *m_mouseTouchAdaptor{nullptr};
@@ -60,4 +50,4 @@ private:
6050 QQmlEngine *m_qmlEngine{nullptr};
6151};
6252
63−#endif // SHELLAPPLICATION_H
53+#endif // UNITYAPPLICATION_H
src/UnityCommandLineParser.cpp+8
@@ -15,6 +15,7 @@
1515 */
1616
1717#include "UnityCommandLineParser.h"
18+#include <paths.h>
1819
1920#include <QDebug>
2021
@@ -60,6 +61,11 @@ UnityCommandLineParser::UnityCommandLineParser(const QCoreApplication &app)
6061 QStringLiteral("mode"), QStringLiteral("full-greeter"));
6162 parser.addOption(modeOption);
6263
64+ QCommandLineOption qmlfileOption(QStringLiteral("qmlfile"),
65+ QStringLiteral("The base qml file to load"),
66+ QStringLiteral("qmlfile"), ::qmlDirectory() + "/ShellApplication.qml");
67+ parser.addOption(qmlfileOption);
68+
6369 // Treat args with single dashes the same as arguments with two dashes
6470 // Ex: -fullscreen == --fullscreen
6571 parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
@@ -85,6 +91,8 @@ UnityCommandLineParser::UnityCommandLineParser(const QCoreApplication &app)
8591 m_hasFullscreen = parser.isSet(fullscreenOption);
8692 m_deviceName = parser.value(devicenameOption);
8793 resolveMode(parser, modeOption);
94+
95+ m_qmlfile = parser.value(qmlfileOption);
8896}
8997
9098int UnityCommandLineParser::parsePixelsValue(const QString &str)
src/UnityCommandLineParser.h+4−1
@@ -36,8 +36,10 @@ public:
3636 bool hasFullscreen() const { return m_hasFullscreen; }
3737 QString deviceName() const { return m_deviceName; }
3838 QString mode() const { return m_mode; }
39−private:
4039
40+ QString qmlfie() const { return m_qmlfile; }
41+
42+protected:
4143 int parsePixelsValue(const QString &str);
4244 static float getenvFloat(const char* name, float defaultValue);
4345 void resolveMode(QCommandLineParser &parser, QCommandLineOption &modeOption);
@@ -55,6 +57,7 @@ private:
5557 bool m_hasFullscreen;
5658 QString m_deviceName;
5759 QString m_mode;
60+ QString m_qmlfile;
5861};
5962
6063#endif // UNITY_COMMAND_LINE_PARSER_H
src/WindowManagementPolicy.cppnew file+69
@@ -0,0 +1,69 @@
1+/*
2+ * Copyright (C) 2017 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+#include "WindowManagementPolicy.h"
18+
19+WindowManagementPolicy::WindowManagementPolicy(const miral::WindowManagerTools &tools, std::shared_ptr<qtmir::WindowManagementPolicyPrivate> dd)
20+ : qtmir::WindowManagementPolicy(tools, dd)
21+ , m_dummyWorkspace(this->tools.create_workspace())
22+{
23+ wmPolicyInterface = this;
24+
25+ // we must always have a active workspace.
26+ m_activeWorkspace = m_dummyWorkspace;
27+}
28+
29+void WindowManagementPolicy::advise_new_window(miral::WindowInfo const& window_info)
30+{
31+ qtmir::WindowManagementPolicy::advise_new_window(window_info);
32+
33+ auto const parent = window_info.parent();
34+
35+ auto activeWorkspace = m_activeWorkspace.lock();
36+ if (!parent && activeWorkspace) {
37+ tools.add_tree_to_workspace(window_info.window(), activeWorkspace);
38+ }
39+}
40+
41+std::shared_ptr<miral::Workspace> WindowManagementPolicy::createWorkspace()
42+{
43+ auto workspace = tools.create_workspace();
44+ m_workspaces.insert(workspace);
45+
46+ if (m_activeWorkspace.lock() == m_dummyWorkspace) {
47+ tools.move_workspace_content_to_workspace(workspace, m_dummyWorkspace);
48+ m_activeWorkspace = workspace;
49+ }
50+ return workspace;
51+}
52+
53+void WindowManagementPolicy::releaseWorkspace(const std::shared_ptr<miral::Workspace> &workspace)
54+{
55+ auto iter = m_workspaces.find(workspace);
56+ if (iter != m_workspaces.end()) m_workspaces.erase(iter);
57+
58+ if (m_workspaces.size() == 0) {
59+ m_activeWorkspace = m_dummyWorkspace;
60+ tools.move_workspace_content_to_workspace(m_dummyWorkspace, workspace);
61+ }
62+}
63+
64+void WindowManagementPolicy::setActiveWorkspace(const std::shared_ptr<miral::Workspace> &workspace)
65+{
66+ if (m_activeWorkspace.lock() == workspace)
67+ return;
68+ m_activeWorkspace = workspace ? workspace : m_dummyWorkspace;
69+}
src/WindowManagementPolicy.hnew file+47
@@ -0,0 +1,47 @@
1+/*
2+ * Copyright (C) 2017 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+#ifndef UNITY_WINDOWMANAGEMENTPOLICY_H
18+#define UNITY_WINDOWMANAGEMENTPOLICY_H
19+
20+#include <qtmir/windowmanagementpolicy.h>
21+#include "wmpolicyinterface.h"
22+
23+#include <unordered_set>
24+
25+class Q_DECL_EXPORT WindowManagementPolicy : public qtmir::WindowManagementPolicy,
26+ public WMPolicyInterface
27+{
28+public:
29+ WindowManagementPolicy(const miral::WindowManagerTools &tools, std::shared_ptr<qtmir::WindowManagementPolicyPrivate> dd);
30+
31+ void advise_new_window(miral::WindowInfo const& window_info) override;
32+
33+ // From WMPolicyInterface
34+ std::shared_ptr<miral::Workspace> createWorkspace() override;
35+
36+ void releaseWorkspace(const std::shared_ptr<miral::Workspace> &workspace) override;
37+
38+ void setActiveWorkspace(const std::shared_ptr<miral::Workspace> &workspace) override;
39+
40+private:
41+ std::weak_ptr<miral::Workspace> m_activeWorkspace;
42+
43+ std::unordered_set<std::shared_ptr<miral::Workspace>> m_workspaces;
44+ const std::shared_ptr<miral::Workspace> m_dummyWorkspace;
45+};
46+
47+#endif // UNITY_WINDOWMANAGEMENTPOLICY_H
src/libunity8-private/CMakeLists.txt+1
@@ -8,6 +8,7 @@ set(lib${LIB_NAME}_SRCS
88 abstractdbusservicemonitor.cpp
99 unitydbusobject.cpp
1010 unitydbusvirtualobject.cpp
11+ wmpolicyinterface.cpp
1112 )
1213
1314add_library(${LIB_NAME} SHARED
src/SecondaryWindow.h → src/libunity8-private/wmpolicyinterface.cpprenamed+6−12
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2017 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -14,17 +14,11 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17−#ifndef UNITY_SECONDARY_WINDOW_H
18−#define UNITY_SECONDARY_WINDOW_H
17+#include "wmpolicyinterface.h"
1918
20−#include <QQuickView>
19+WMPolicyInterface* wmPolicyInterface = nullptr;
2120
22−class SecondaryWindow : public QQuickView
21+WMPolicyInterface *WMPolicyInterface::instance()
2322{
24− Q_OBJECT
25−
26−public:
27− SecondaryWindow(QQmlEngine *engine);
28−};
29−
30−#endif // UNITY_SECONDARY_WINDOW_H
23+ return wmPolicyInterface;
24+}
src/libunity8-private/wmpolicyinterface.hnew file+46
@@ -0,0 +1,46 @@
1+/*
2+ * Copyright (C) 2017 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+#ifndef WMPOLICYINTERFACE_H
18+#define WMPOLICYINTERFACE_H
19+
20+#include <memory>
21+#include <functional>
22+
23+#include <qglobal.h>
24+
25+namespace miral {
26+class Workspace;
27+class Window;
28+}
29+
30+class Q_DECL_EXPORT WMPolicyInterface
31+{
32+public:
33+ virtual ~WMPolicyInterface() {}
34+
35+ static WMPolicyInterface *instance();
36+
37+ virtual std::shared_ptr<miral::Workspace> createWorkspace() = 0;
38+
39+ virtual void releaseWorkspace(const std::shared_ptr<miral::Workspace> &workspace) = 0;
40+
41+ virtual void setActiveWorkspace(const std::shared_ptr<miral::Workspace> &workspace) = 0;
42+};
43+
44+extern Q_DECL_EXPORT WMPolicyInterface* wmPolicyInterface;
45+
46+#endif // WMPOLICYINTERFACE_H
src/main.cpp+4−8
@@ -15,9 +15,10 @@
1515 */
1616
1717// local
18−#include "ShellApplication.h"
18+#include "UnityApplication.h"
1919#include "qmldebuggerutils.h"
2020#include "UnixSignalHandler.h"
21+#include <paths.h>
2122
2223#include <QTranslator>
2324#include <QLibraryInfo>
@@ -27,17 +28,12 @@ int main(int argc, const char *argv[])
2728{
2829 qSetMessagePattern("[%{time yyyy-MM-dd:hh:mm:ss.zzz}] %{if-category}%{category}: %{endif}%{message}");
2930
30− bool isMirServer = qgetenv("QT_QPA_PLATFORM") == "mirserver";
31− if (!isMirServer && qgetenv("QT_QPA_PLATFORM") == "ubuntumirclient") {
32− setenv("QT_QPA_PLATFORM", "mirserver", 1 /* overwrite */);
33− isMirServer = true;
34− }
35−
3631 if (enableQmlDebugger(argc, argv)) {
3732 QQmlDebuggingEnabler qQmlEnableDebuggingHelper(true);
3833 }
3934
40− ShellApplication *application = new ShellApplication(argc, (char**)argv, isMirServer);
35+ auto *application = new UnityApplication(argc,
36+ (char**)argv);
4137
4238 UnixSignalHandler signalHandler([]{
4339 QGuiApplication::exit(0);
tests/CMakeLists.txt+1
@@ -47,6 +47,7 @@ macro(unity8_parse_arguments)
4747 list(APPEND ld_paths
4848 ${UNITY_MOCKPATH}/liblightdm
4949 ${UNITY_MOCKPATH}/libusermetrics
50+ ${UNITY_MOCKPATH}/WindowManager
5051 )
5152
5253 string(REPLACE ";" ":" ld_library_path "${ld_paths}")
tests/mocks/CMakeLists.txt+1
@@ -43,6 +43,7 @@ add_subdirectory(SessionBroadcast)
4343add_subdirectory(Ubuntu)
4444add_subdirectory(Unity)
4545add_subdirectory(QtMultimedia)
46+add_subdirectory(WindowManager)
4647add_subdirectory(Wizard)
4748add_subdirectory(Utils)
4849add_subdirectory(UInput)
tests/mocks/Cursor/Cursor.qml+49−2
@@ -15,8 +15,11 @@
1515 */
1616
1717import QtQuick 2.4
18+import UInput 0.1
19+
20+Canvas {
21+ id: root
1822
19−Item {
2023 property int topBoundaryOffset // effectively panel height
2124 property Item confiningItem
2225
@@ -30,5 +33,49 @@ Item {
3033 signal pushStopped()
3134 signal mouseMoved()
3235
33− onMouseMoved: opacity = 1;
36+ width: units.gu(2)
37+ height: units.gu(2)
38+ antialiasing: true
39+
40+ onPaint: {
41+ var ctx = getContext("2d");
42+ ctx.save();
43+ ctx.clearRect(0,0,width, height);
44+ ctx.strokeStyle = "#000000";
45+ ctx.lineWidth = 1
46+ ctx.fillStyle = "#ffffff";
47+ ctx.globalAlpha = 1.0;
48+ ctx.lineJoin = "round";
49+ ctx.beginPath();
50+
51+ // put rectangle in the middle
52+ // draw the rectangle
53+ ctx.moveTo(width/2,height);
54+ ctx.lineTo(width, height/2);
55+ ctx.lineTo(0,0);
56+
57+ ctx.closePath();
58+ ctx.fill();
59+ ctx.stroke();
60+ ctx.restore();
61+ }
62+
63+ Connections {
64+ target: UInput
65+ onMouseMoved: {
66+ var newX = root.x;
67+ newX += dx;
68+ if (newX < 0) newX = 0;
69+ else if (newX >= parent.width) newX = parent.width-1;
70+
71+ var newY = root.y;
72+ newY += dy;
73+ if (newY < 0) newY = 0;
74+ else if (newY >= parent.height) newY = parent.height-1;
75+
76+ root.x = newX;
77+ root.y = newY;
78+ root.mouseMoved();
79+ }
80+ }
3481}
tests/mocks/GSettings.1.0/plugin.cpp+12
@@ -18,6 +18,8 @@
1818#include "fake_gsettings.h"
1919
2020#include <QtQml/qqml.h>
21+#include <QQmlEngine>
22+#include <QQmlContext>
2123
2224static QObject* controllerProvider(QQmlEngine* /* engine */, QJSEngine* /* scriptEngine */)
2325{
@@ -31,3 +33,13 @@ void FakeGSettingsQmlPlugin::registerTypes(const char *uri)
3133 qmlRegisterUncreatableType<GSettingsSchemaQml>(uri, 1, 0, "GSettingsSchema",
3234 "GSettingsSchema can only be used inside of a GSettings component");
3335}
36+
37+void FakeGSettingsQmlPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
38+{
39+ QQmlExtensionPlugin::initializeEngine(engine, uri);
40+
41+ QString usageType = qgetenv("UNITY_MOCK_DESKTOP");
42+ if (!usageType.isEmpty()) {
43+ GSettingsControllerQml::instance()->setUsageMode("Windowed");
44+ }
45+}
tests/mocks/GSettings.1.0/plugin.h+1
@@ -26,6 +26,7 @@ class FakeGSettingsQmlPlugin : public QQmlExtensionPlugin
2626
2727public:
2828 void registerTypes(const char *uri) override;
29+ void initializeEngine(QQmlEngine *engine, const char *uri) override;
2930};
3031
3132#endif // PLUGIN_H
tests/mocks/UInput/plugin.cpp+6−1
@@ -19,8 +19,13 @@
1919
2020#include <QtQml/qqml.h>
2121
22+QObject* uinputSingleton(QQmlEngine*, QJSEngine*)
23+{
24+ return new MockUInput;
25+}
26+
2227void MockUInputPlugin::registerTypes(const char *uri)
2328{
2429 Q_ASSERT(uri == QLatin1String("UInput"));
25− qmlRegisterType<MockUInput>(uri, 0, 1, "UInput");
30+ qmlRegisterSingletonType<MockUInput>(uri, 0, 1, "UInput", uinputSingleton);
2631}
tests/mocks/Unity/Application/CMakeLists.txt+26−1
@@ -1,3 +1,12 @@
1+pkg_check_modules(MIRTEST mirtest>=0.26 REQUIRED)
2+
3+include_directories(
4+ SYSTEM
5+ ${MIRTEST_INCLUDE_DIRS}
6+ ${MIRAL_INCLUDE_DIRS}
7+ ${libunity8-private_SOURCE_DIR}
8+)
9+
110set(FakeUnityApplicationQml_SOURCES
211 plugin.cpp
312 ApplicationInfo.cpp
@@ -19,11 +28,27 @@ set(FakeUnityApplicationQml_SOURCES
1928 resources/surfaces.qrc
2029)
2130
22−add_library(FakeUnityApplicationQml MODULE ${FakeUnityApplicationQml_SOURCES})
31+add_library(FakeUnityApplicationQml MODULE
32+ ${FakeUnityApplicationQml_SOURCES}
33+)
34+
35+#add_dependencies(FakeUnityApplicationQml windowmanagementpolicy)
36+
37+target_link_libraries(FakeUnityApplicationQml
38+ ${MIRTEST_LDFLAGS}
39+ ${MIRAL_LDFLAGS}
40+ mockwindowmanagmentpolicy
41+)
2342
2443add_library(NonMirUnityApplicationQml MODULE ${FakeUnityApplicationQml_SOURCES})
2544set_target_properties(NonMirUnityApplicationQml PROPERTIES OUTPUT_NAME FakeUnityApplicationQml)
2645
46+target_link_libraries(NonMirUnityApplicationQml
47+ ${MIRTEST_LDFLAGS}
48+ ${MIRAL_LDFLAGS}
49+ mockwindowmanagmentpolicy
50+)
51+
2752qt5_use_modules(FakeUnityApplicationQml Core Quick DBus)
2853qt5_use_modules(NonMirUnityApplicationQml Core Quick DBus)
2954
tests/mocks/Unity/Application/MirSurfaceItem.cpp+26−7
@@ -189,11 +189,13 @@ void MirSurfaceItem::createQmlContentItem()
189189 m_qmlItem = qobject_cast<QQuickItem*>(m_qmlContentComponent->create());
190190 m_qmlItem->setParentItem(this);
191191
192− m_qmlItem->setWidth(m_surfaceWidth);
193− m_qmlItem->setHeight(m_surfaceHeight);
194−
195− setImplicitWidth(m_qmlItem->implicitWidth());
196− setImplicitHeight(m_qmlItem->implicitHeight());
192+ if (m_fillMode == FillMode::Stretch && width() != 0 && height() != 0) {
193+ m_qmlItem->setSize(QSize(this->width(), this->height()));
194+ } else {
195+ m_qmlItem->setSize(m_qmlSurface->size());
196+ }
197+ setImplicitWidth(m_qmlItem->width());
198+ setImplicitHeight(m_qmlItem->height());
197199
198200 {
199201 QQmlProperty screenshotSource(m_qmlItem, "screenshotSource");
@@ -308,6 +310,16 @@ void MirSurfaceItem::setSurface(MirSurfaceInterface* surface)
308310 connect(m_qmlSurface, &MirSurface::screenshotUrlChanged, this, &MirSurfaceItem::updateScreenshot);
309311 connect(m_qmlSurface, &MirSurface::liveChanged, this, &MirSurfaceItem::liveChanged);
310312 connect(m_qmlSurface, &MirSurface::stateChanged, this, &MirSurfaceItem::surfaceStateChanged);
313+ connect(m_qmlSurface, &MirSurface::sizeChanged, this, [this] () {
314+ setImplicitSize(m_qmlSurface->width(), m_qmlSurface->height());
315+ if (m_fillMode == FillMode::Stretch) {
316+ m_qmlItem->setSize(QSize(this->width(), this->height()));
317+ } else {
318+ m_qmlItem->setSize(m_qmlSurface->size());
319+ }
320+ });
321+ m_surfaceWidth = surface->size().width();
322+ m_surfaceHeight = surface->size().height();
311323
312324 QUrl qmlComponentFilePath;
313325 if (!m_qmlSurface->qmlFilePath().isEmpty()) {
@@ -321,6 +333,7 @@ void MirSurfaceItem::setSurface(MirSurfaceInterface* surface)
321333 switch (m_qmlContentComponent->status()) {
322334 case QQmlComponent::Ready:
323335 createQmlContentItem();
336+ qDebug() << "content created" << m_surfaceWidth << implicitWidth() << width();
324337 break;
325338 case QQmlComponent::Loading:
326339 connect(m_qmlContentComponent, &QQmlComponent::statusChanged,
@@ -399,9 +412,15 @@ void MirSurfaceItem::updateSurfaceSize()
399412 if (m_qmlSurface && m_surfaceWidth > 0 && m_surfaceHeight > 0) {
400413 m_qmlSurface->resize(m_surfaceWidth, m_surfaceHeight);
401414 if (m_qmlItem) {
402− m_qmlItem->setWidth(m_surfaceWidth);
403− m_qmlItem->setHeight(m_surfaceHeight);
415+ if (m_fillMode == FillMode::Stretch) {
416+ m_qmlItem->setWidth(width());
417+ m_qmlItem->setHeight(height());
418+ } else {
419+ m_qmlItem->setWidth(m_surfaceWidth);
420+ m_qmlItem->setHeight(m_surfaceHeight);
421+ }
404422 }
423+ qDebug() << this << "setting implicitsize" << m_surfaceWidth << m_surfaceHeight;
405424 setImplicitSize(m_surfaceWidth, m_surfaceHeight);
406425 }
407426}
tests/mocks/Unity/Application/SurfaceManager.cpp+118−11
@@ -18,10 +18,12 @@
1818
1919#include "ApplicationInfo.h"
2020#include "VirtualKeyboard.h"
21+#include "../../WindowManager/WindowManagementPolicy.h"
2122
2223#include <paths.h>
24+#include <mirtest/mir/test/doubles/stub_surface.h>
2325
24−#define SURFACEMANAGER_DEBUG 0
26+#define SURFACEMANAGER_DEBUG 1
2527
2628#if SURFACEMANAGER_DEBUG
2729#define DEBUG_MSG(params) qDebug().nospace() << "SurfaceManager[" << (void*)this << "]::" << __func__ << params
@@ -31,6 +33,12 @@
3133
3234namespace unityapi = unity::shell::application;
3335
36+uint qHash(const WindowWrapper &key, uint)
37+{
38+ std::shared_ptr<mir::scene::Surface> surface = key.window;
39+ return (quintptr)surface.get();
40+}
41+
3442SurfaceManager *SurfaceManager::m_instance = nullptr;
3543
3644SurfaceManager *SurfaceManager::instance()
@@ -44,20 +52,63 @@ SurfaceManager::SurfaceManager(QObject *)
4452
4553 Q_ASSERT(m_instance == nullptr);
4654 m_instance = this;
55+
56+ connect(WindowManagementPolicy::instance(), &WindowManagementPolicy::windowAdded,
57+ this, [this](const miral::Window& window) {
58+ Q_EMIT surfaceCreated(surfaceFor(window));
59+ });
60+
61+ connect(WindowManagementPolicy::instance(), &WindowManagementPolicy::windowsAddedToWorkspace,
62+ this, [this](const std::shared_ptr<miral::Workspace> &workspace, const std::vector<miral::Window> &windows) {
63+ Q_EMIT surfacesAddedToWorkspace(workspace, surfacesFor(windows));
64+ });
65+
66+ connect(WindowManagementPolicy::instance(), &WindowManagementPolicy::windowsAboutToBeRemovedFromWorkspace,
67+ this, [this](const std::shared_ptr<miral::Workspace> &workspace, const std::vector<miral::Window> &windows) {
68+ Q_EMIT surfacesAboutToBeRemovedFromWorkspace(workspace, surfacesFor(windows));
69+ });
4770}
4871
4972SurfaceManager::~SurfaceManager()
5073{
5174 DEBUG_MSG("");
5275
53− if (m_virtualKeyboard) {
54− m_virtualKeyboard->setLive(false);
55− }
76+ releaseInputMethodSurface();
5677
5778 Q_ASSERT(m_instance == this);
5879 m_instance = nullptr;
5980}
6081
82+MirSurfaceInterface *SurfaceManager::surfaceFor(const miral::Window& window) const
83+{
84+ auto iter = m_windowToSurface.find({window});
85+ if (iter != m_windowToSurface.end()) {
86+ return *iter;
87+ }
88+ return nullptr;
89+}
90+
91+QVector<MirSurfaceInterface*> SurfaceManager::surfacesFor(const std::vector<miral::Window> &windows) const
92+{
93+ QVector<unityapi::MirSurfaceInterface*> surfaces;
94+ for (size_t i = 0; i < windows.size(); i++) {
95+ auto mirSurface = surfaceFor(windows[i]);
96+ if (mirSurface) {
97+ surfaces.push_back(mirSurface);
98+ }
99+ }
100+ return surfaces;
101+}
102+
103+miral::Window SurfaceManager::windowFor(MirSurfaceInterface *surface) const
104+{
105+ auto iter = m_surfaceToWindow.find(surface);
106+ if (iter != m_surfaceToWindow.end()) {
107+ return iter->window;
108+ }
109+ return miral::Window();
110+}
111+
61112MirSurface *SurfaceManager::createSurface(const QString& name,
62113 Mir::Type type,
63114 Mir::State state,
@@ -75,7 +126,12 @@ MirSurface *SurfaceManager::createSurface(const QString& name,
75126
76127void SurfaceManager::registerSurface(MirSurface *surface)
77128{
129+ auto fakeSurface = std::make_shared<mir::test::doubles::StubSurface>();
130+ WindowWrapper window{miral::Window(nullptr, fakeSurface), fakeSurface};
131+
78132 m_surfaces.prepend(surface);
133+ m_windowToSurface.insert(window, surface);
134+ m_surfaceToWindow.insert(surface, window);
79135
80136 if (!surface->parentSurface()) {
81137 surface->setMinimumWidth(m_newSurfaceMinimumWidth);
@@ -90,16 +146,24 @@ void SurfaceManager::registerSurface(MirSurface *surface)
90146 this->onStateRequested(surface, state);
91147 });
92148
93− const QString persistentId = surface->persistentId();
94149 connect(surface, &QObject::destroyed, this, [=]() {
95− this->onSurfaceDestroyed(surface, persistentId);
150+ auto iter = m_surfaceToWindow.find(surface);
151+ if (iter != m_surfaceToWindow.end()) {
152+ WindowWrapper key = m_surfaceToWindow.value(surface);
153+ WindowManagementPolicy::instance()->removeWindow(key.window);
154+ this->onSurfaceDestroyed(surface);
155+ }
96156 });
97−
98157}
99158
100159void SurfaceManager::notifySurfaceCreated(unityapi::MirSurfaceInterface *surface)
101160{
102− Q_EMIT surfaceCreated(surface);
161+ if (Q_UNLIKELY(!m_surfaceToWindow.contains(surface))) {
162+ Q_EMIT surfaceCreated(surface);
163+ return;
164+ }
165+
166+ WindowManagementPolicy::instance()->addWindow(m_surfaceToWindow[surface].window);
103167}
104168
105169void SurfaceManager::setNewSurfaceMinimumWidth(int value)
@@ -209,6 +273,32 @@ void SurfaceManager::activate(unityapi::MirSurfaceInterface *apiSurface)
209273 DEBUG_MSG("("<<surface<<") ended");
210274}
211275
276+void SurfaceManager::forEachSurfaceInWorkspace(const std::shared_ptr<miral::Workspace> &workspace,
277+ const std::function<void(unity::shell::application::MirSurfaceInterface*)> &callback)
278+{
279+ WindowManagementPolicy::instance()->forEachWindowInWorkspace(workspace, [&](const miral::Window &window) {
280+ auto surface = surfaceFor(window);
281+ if (surface) {
282+ callback(surface);
283+ }
284+ });
285+}
286+
287+void SurfaceManager::moveSurfaceToWorkspace(unity::shell::application::MirSurfaceInterface* surface,
288+ const std::shared_ptr<miral::Workspace> &workspace)
289+{
290+ auto window = windowFor(surface);
291+ if (window) {
292+ WindowManagementPolicy::instance()->moveWindowToWorkspace(window, workspace);
293+ }
294+}
295+
296+void SurfaceManager::moveWorkspaceContentToWorkspace(const std::shared_ptr<miral::Workspace> &to,
297+ const std::shared_ptr<miral::Workspace> &from)
298+{
299+ WindowManagementPolicy::instance()->moveWorkspaceContentToWorkspace(to, from);
300+}
301+
212302void SurfaceManager::onStateRequested(MirSurface *surface, Mir::State state)
213303{
214304 DEBUG_MSG("("<<surface<<","<<state<<") started");
@@ -230,9 +320,17 @@ void SurfaceManager::onStateRequested(MirSurface *surface, Mir::State state)
230320 DEBUG_MSG("("<<surface<<","<<state<<") ended");
231321}
232322
233−void SurfaceManager::onSurfaceDestroyed(MirSurface *surface, const QString& persistentId)
323+void SurfaceManager::onSurfaceDestroyed(MirSurface *surface)
234324{
235325 m_surfaces.removeAll(surface);
326+
327+ auto iter = m_surfaceToWindow.find(surface);
328+ if (iter != m_surfaceToWindow.end()) {
329+ WindowWrapper key = iter.value();
330+ m_windowToSurface.remove(key);
331+ m_surfaceToWindow.erase(iter);
332+ }
333+
236334 if (m_focusedSurface == surface) {
237335 m_focusedSurface = nullptr;
238336
@@ -244,7 +342,7 @@ void SurfaceManager::onSurfaceDestroyed(MirSurface *surface, const QString& pers
244342 m_underModification = false;
245343 Q_EMIT modificationsEnded();
246344 }
247− Q_EMIT surfaceDestroyed(persistentId);
345+ Q_EMIT surfaceRemoved(surface);
248346}
249347
250348void SurfaceManager::focusFirstAvailableSurface()
@@ -276,6 +374,15 @@ void SurfaceManager::createInputMethodSurface()
276374 if (!m_virtualKeyboard) {
277375 m_virtualKeyboard = new VirtualKeyboard;
278376 registerSurface(m_virtualKeyboard);
279− Q_EMIT surfaceCreated(m_virtualKeyboard);
377+
378+ WindowManagementPolicy::instance()->addWindow(m_surfaceToWindow[m_virtualKeyboard].window);
379+ }
380+}
381+
382+void SurfaceManager::releaseInputMethodSurface()
383+{
384+ if (m_virtualKeyboard) {
385+ m_virtualKeyboard->setLive(false);
386+ m_virtualKeyboard = nullptr;
280387 }
281388}
tests/mocks/Unity/Application/SurfaceManager.h+23−3
@@ -20,12 +20,20 @@
2020#include <QObject>
2121
2222#include <unity/shell/application/SurfaceManagerInterface.h>
23+#include <miral/window.h>
2324
2425#include "MirSurface.h"
2526#include "VirtualKeyboard.h"
2627
2728class ApplicationInfo;
2829
30+struct WindowWrapper {
31+ miral::Window window;
32+ std::shared_ptr<mir::scene::Surface> session{nullptr}; // Keeps the window surface alive.
33+ bool operator==(const WindowWrapper& other) const { return window==other.window; }
34+};
35+uint qHash(const WindowWrapper &key, uint seed = 0);
36+
2937class SurfaceManager : public unity::shell::application::SurfaceManagerInterface
3038{
3139 Q_OBJECT
@@ -46,6 +54,13 @@ public:
4654 void raise(unity::shell::application::MirSurfaceInterface *surface) override;
4755 void activate(unity::shell::application::MirSurfaceInterface *surface) override;
4856
57+ void forEachSurfaceInWorkspace(const std::shared_ptr<miral::Workspace> &workspace,
58+ const std::function<void(unity::shell::application::MirSurfaceInterface*)> &callback) override;
59+ void moveSurfaceToWorkspace(unity::shell::application::MirSurfaceInterface* surface,
60+ const std::shared_ptr<miral::Workspace> &workspace) override;
61+ void moveWorkspaceContentToWorkspace(const std::shared_ptr<miral::Workspace> &to,
62+ const std::shared_ptr<miral::Workspace> &from) override;
63+
4964 Q_INVOKABLE MirSurface* createSurface(const QString& name,
5065 Mir::Type type,
5166 Mir::State state,
@@ -76,10 +91,9 @@ public:
7691
7792public Q_SLOTS:
7893 void createInputMethodSurface();
94+ void releaseInputMethodSurface();
7995
8096Q_SIGNALS:
81− void surfaceDestroyed(const QString& persistentSurfaceId);
82−
8397 void newSurfaceMinimumWidthChanged(int value);
8498 void newSurfaceMaximumWidthChanged(int value);
8599 void newSurfaceMinimumHeightChanged(int value);
@@ -89,12 +103,15 @@ Q_SIGNALS:
89103
90104private Q_SLOTS:
91105 void onStateRequested(MirSurface *surface, Mir::State state);
92− void onSurfaceDestroyed(MirSurface *surface, const QString& persistentId);
106+ void onSurfaceDestroyed(MirSurface *surface);
93107
94108private:
95109 void doRaise(unity::shell::application::MirSurfaceInterface *surface);
96110 void focusFirstAvailableSurface();
97111 void registerSurface(MirSurface *surface);
112+ unity::shell::application::MirSurfaceInterface* surfaceFor(const miral::Window &window) const;
113+ QVector<unity::shell::application::MirSurfaceInterface*> surfacesFor(const std::vector<miral::Window> &windows) const;
114+ miral::Window windowFor(unity::shell::application::MirSurfaceInterface* surface) const;
98115
99116 static SurfaceManager *m_instance;
100117
@@ -110,6 +127,9 @@ private:
110127
111128 QList<MirSurface*> m_surfaces;
112129
130+ QHash<WindowWrapper, unity::shell::application::MirSurfaceInterface*> m_windowToSurface;
131+ QHash<unity::shell::application::MirSurfaceInterface*, WindowWrapper> m_surfaceToWindow;
132+
113133 VirtualKeyboard *m_virtualKeyboard{nullptr};
114134};
115135
tests/mocks/Unity/Application/plugin.cpp+7−1
@@ -38,6 +38,12 @@ QObject* mirSingleton(QQmlEngine*, QJSEngine*)
3838{
3939 return new MirMock;
4040}
41+
42+QObject* surfaceManagerSingleton(QQmlEngine*, QJSEngine*)
43+{
44+ return new SurfaceManager;
45+}
46+
4147} // anonymous namespace
4248
4349void FakeUnityApplicationQmlPlugin::registerTypes(const char *uri)
@@ -59,7 +65,7 @@ void FakeUnityApplicationQmlPlugin::registerTypes(const char *uri)
5965
6066 qmlRegisterSingletonType<ApplicationManager>(uri, 0, 1, "ApplicationManager", applicationManagerSingleton);
6167 qmlRegisterSingletonType<MirMock>(uri, 0, 1, "Mir", mirSingleton);
62− qmlRegisterType<SurfaceManager>(uri, 0, 1, "SurfaceManager");
68+ qmlRegisterSingletonType<SurfaceManager>(uri, 0, 1, "SurfaceManager", surfaceManagerSingleton);
6369}
6470
6571void FakeUnityApplicationQmlPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
tests/mocks/Unity/Application/resources/MirSurfaceItem.qml−3
@@ -20,9 +20,6 @@ Rectangle {
2020 id: root
2121 color: "pink"
2222
23− implicitWidth: width
24− implicitHeight: height
25−
2623 property alias screenshotSource: screenshotImage.source
2724 property int orientationAngle
2825
tests/mocks/Unity/CMakeLists.txt−1
@@ -5,5 +5,4 @@ add_subdirectory(Indicators)
55add_subdirectory(InputInfo)
66add_subdirectory(Launcher)
77add_subdirectory(Notifications)
8−add_subdirectory(Screens)
98add_subdirectory(Platform)
tests/mocks/Unity/InputInfo/mockcontroller.cpp+10
@@ -18,10 +18,20 @@
1818
1919#include "qinputdeviceinfo_mock_p.h"
2020
21+#include <QQuickView>
22+#include <QQmlComponent>
23+#include <QQuickItem>
24+#include <paths.h>
25+
2126MockController::MockController(QObject *parent):
2227 QObject(parent)
2328{
29+}
2430
31+MockController *MockController::instance()
32+{
33+ static MockController* controller = new MockController();
34+ return controller;
2535}
2636
2737QInputDevice *MockController::addMockDevice(const QString &devicePath, QInputDevice::InputType type)
tests/mocks/Unity/InputInfo/mockcontroller.h+3−1
@@ -24,9 +24,11 @@ class MockController: public QObject
2424{
2525 Q_OBJECT
2626public:
27− MockController(QObject *parent = 0);
27+ MockController(QObject *parent = nullptr);
2828 ~MockController() = default;
2929
30+ static MockController *instance();
31+
3032 Q_INVOKABLE QInputDevice* addMockDevice(const QString &devicePath, QInputDevice::InputType type);
3133 Q_INVOKABLE void removeDevice(const QString &devicePath);
3234};
tests/mocks/Unity/InputInfo/plugin.cpp+1−2
@@ -26,9 +26,8 @@
2626
2727static QObject *backendProvider(QQmlEngine *engine, QJSEngine *scriptEngine)
2828{
29− Q_UNUSED(engine)
3029 Q_UNUSED(scriptEngine)
31− return new MockController(engine);
30+ return MockController::instance();
3231}
3332
3433void InputInfoPlugin::registerTypes(const char *uri)
tests/mocks/Unity/Screens/CMakeLists.txtdeleted−14
@@ -1,14 +0,0 @@
1−include_directories(
2− ${CMAKE_CURRENT_SOURCE_DIR}
3−)
4−
5−set(MockScreens_SOURCES
6− plugin.cpp
7− screens.cpp
8−)
9−
10−add_library(MockScreensPlugin MODULE ${MockScreens_SOURCES})
11−
12−qt5_use_modules(MockScreensPlugin Gui Qml)
13−
14−add_unity8_mock(Unity.Screens 0.1 Unity/Screens PREFIX mocks TARGETS MockScreensPlugin)
tests/mocks/Unity/Screens/qmldirdeleted−2
@@ -1,2 +0,0 @@
1−module Unity.Screens
2−plugin MockScreensPlugin
tests/mocks/Unity/Screens/screens.cppdeleted−71
@@ -1,71 +0,0 @@
1−/*
2− * Copyright (C) 2015 Canonical, Ltd.
3− *
4− * This program is free software: you can redistribute it and/or modify it under
5− * the terms of the GNU Lesser General Public License version 3, as published by
6− * the Free Software Foundation.
7− *
8− * This program is distributed in the hope that it will be useful, but WITHOUT
9− * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10− * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11− * Lesser General Public License for more details.
12− *
13− * You should have received a copy of the GNU Lesser General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#include "screens.h"
18−
19−// Qt
20−#include <QGuiApplication>
21−#include <QScreen>
22−#include <QDebug>
23−
24−Q_DECLARE_METATYPE(QScreen*)
25−
26−Screens::Screens(QObject *parent) :
27− QAbstractListModel(parent)
28−{
29− // start with one screen attached
30− m_screenList.append(new Screen());
31−}
32−
33−Screens::~Screens() noexcept
34−{
35− qDeleteAll(m_screenList);
36− m_screenList.clear();
37−}
38−
39−QHash<int, QByteArray> Screens::roleNames() const
40−{
41− QHash<int, QByteArray> roles;
42− roles[ScreenRole] = "screen";
43− roles[OutputTypeRole] = "outputType";
44− return roles;
45−}
46−
47−QVariant Screens::data(const QModelIndex &index, int role) const
48−{
49− if (!index.isValid() || index.row() >= m_screenList.size()) {
50− return QVariant();
51− }
52−
53− switch(role) {
54− case ScreenRole:
55− return QVariant::fromValue(m_screenList.at(index.row())->qScreen);
56− case OutputTypeRole:
57− return m_screenList.at(index.row())->outputTypes;
58− }
59−
60− return QVariant();
61−}
62−
63−int Screens::rowCount(const QModelIndex &) const
64−{
65− return count();
66−}
67−
68−int Screens::count() const
69−{
70− return m_screenList.size();
71−}
tests/mocks/Unity/Screens/screens.hdeleted−82
@@ -1,82 +0,0 @@
1−/*
2− * Copyright (C) 2015 Canonical, Ltd.
3− *
4− * This program is free software: you can redistribute it and/or modify it under
5− * the terms of the GNU Lesser General Public License version 3, as published by
6− * the Free Software Foundation.
7− *
8− * This program is distributed in the hope that it will be useful, but WITHOUT
9− * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10− * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11− * Lesser General Public License for more details.
12− *
13− * You should have received a copy of the GNU Lesser General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#ifndef SCREENS_H
18−#define SCREENS_H
19−
20−#include <QAbstractListModel>
21−#include <QScreen>
22−
23−class Screen;
24−
25−class Screens : public QAbstractListModel
26−{
27− Q_OBJECT
28−
29− Q_PROPERTY(int count READ count NOTIFY countChanged)
30−
31−public:
32− enum ItemRoles {
33− ScreenRole = Qt::UserRole + 1,
34− OutputTypeRole
35− };
36−
37− enum OutputTypes {
38− Unknown,
39− VGA,
40− DVII,
41− DVID,
42− DVIA,
43− Composite,
44− SVideo,
45− LVDS,
46− Component,
47− NinePinDIN,
48− DisplayPort,
49− HDMIA,
50− HDMIB,
51− TV,
52− EDP
53− };
54− Q_ENUM(OutputTypes)
55−
56− explicit Screens(QObject *parent = 0);
57− virtual ~Screens() noexcept;
58−
59− /* QAbstractItemModel */
60− QHash<int, QByteArray> roleNames() const override;
61− QVariant data(const QModelIndex &index, int role = ScreenRole) const override;
62− int rowCount(const QModelIndex &parent = QModelIndex()) const override;
63−
64− int count() const;
65−
66−Q_SIGNALS:
67− void countChanged();
68− void screenAdded(QScreen *screen);
69− void screenRemoved(QScreen *screen);
70−
71−private:
72− QList<Screen *> m_screenList;
73−};
74−
75−class Screen
76−{
77−public:
78− Screens::OutputTypes outputTypes = Screens::Unknown;
79− QScreen *qScreen = nullptr;
80−};
81−
82−#endif // SCREENS_H
tests/mocks/WindowManager/CMakeLists.txtnew file+61
@@ -0,0 +1,61 @@
1+include_directories(
2+ SYSTEM
3+ ${QTMIRSERVER_INCLUDE_DIRS}
4+ ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
5+)
6+
7+include_directories(
8+ ${CMAKE_CURRENT_SOURCE_DIR}
9+ ${CMAKE_CURRENT_BINARY_DIR}
10+ ${CMAKE_SOURCE_DIR}/plugins/WindowManager
11+ ${libunity8-private_SOURCE_DIR}
12+)
13+
14+add_library(mockwindowmanagmentpolicy SHARED
15+ WindowManagementPolicy.cpp
16+)
17+target_link_libraries(mockwindowmanagmentpolicy
18+ ${MIRAL_LDFLAGS}
19+ unity8-private
20+)
21+qt5_use_modules(mockwindowmanagmentpolicy Core)
22+
23+install(TARGETS mockwindowmanagmentpolicy
24+ DESTINATION ${SHELL_INSTALL_QML}/mocks/WindowManager
25+ )
26+
27+set(WINDOWMANAGER_SRC
28+ ${CMAKE_SOURCE_DIR}/plugins/WindowManager/AvailableDesktopArea.cpp
29+ ${CMAKE_SOURCE_DIR}/plugins/WindowManager/Screen.cpp
30+ ${CMAKE_SOURCE_DIR}/plugins/WindowManager/ScreenAttached.cpp
31+ ${CMAKE_SOURCE_DIR}/plugins/WindowManager/Screens.cpp
32+ ${CMAKE_SOURCE_DIR}/plugins/WindowManager/ScreenWindow.cpp
33+ ${CMAKE_SOURCE_DIR}/plugins/WindowManager/TopLevelWindowModel.cpp
34+ ${CMAKE_SOURCE_DIR}/plugins/WindowManager/Window.cpp
35+ ${CMAKE_SOURCE_DIR}/plugins/WindowManager/WindowMargins.cpp
36+ ${CMAKE_SOURCE_DIR}/plugins/WindowManager/WindowManagerObjects.cpp
37+ ${CMAKE_SOURCE_DIR}/plugins/WindowManager/WorkspaceManager.cpp
38+ ${CMAKE_SOURCE_DIR}/plugins/WindowManager/WorkspaceModel.cpp
39+ ${CMAKE_SOURCE_DIR}/plugins/WindowManager/Workspace.cpp
40+ MockScreens.cpp
41+ MockScreenWindow.cpp
42+ MockScreensConfiguration.cpp
43+ WindowManagerPlugin.cpp
44+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
45+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
46+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/Mir.h
47+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceInterface.h
48+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceListInterface.h
49+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/SurfaceManagerInterface.h
50+)
51+
52+add_library(MockWindowManager-qml SHARED ${WINDOWMANAGER_SRC})
53+
54+target_link_libraries(MockWindowManager-qml
55+ ${QTMIRSERVER_LDFLAGS}
56+ mockwindowmanagmentpolicy
57+)
58+
59+qt5_use_modules(MockWindowManager-qml Qml Quick Gui)
60+
61+add_unity8_mock(WindowManager 1.0 WindowManager TARGETS MockWindowManager-qml)
tests/mocks/Unity/Screens/plugin.cpp → tests/mocks/WindowManager/MockScreenWindow.cpprenamed+14−9
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2016-2017 Canonical, Ltd.
33 *
44 * This program is free software: you can redistribute it and/or modify it under
55 * the terms of the GNU Lesser General Public License version 3, as published by
@@ -14,16 +14,21 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17−#include "plugin.h"
18−#include "screens.h"
17+#include "MockScreenWindow.h"
18+#include "MockScreens.h"
1919
20−#include <QScreen>
20+// Qt
21+#include <QGuiApplication>
22+#include <QDebug>
2123
22−void UnityScreensPlugin::registerTypes(const char* uri)
24+MockScreenWindow::MockScreenWindow(QQuickWindow *parent)
25+ : ScreenWindow(parent)
2326{
24− Q_ASSERT(QLatin1String(uri) == QLatin1String("Unity.Screens"));
25−
26− qRegisterMetaType<QScreen*>("QScreen*");
27+ connect(this, &ScreenWindow::screenWrapperChanged, this, [this]() {
28+ MockScreens::instance()->connectWindow(this);
29+ });
30+}
2731
28− qmlRegisterType<Screens>(uri, 0, 1, "Screens");
32+MockScreenWindow::~MockScreenWindow()
33+{
2934}
tests/mocks/Unity/Screens/plugin.h → tests/mocks/WindowManager/MockScreenWindow.hrenamed+11−6
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2016-2017 Canonical, Ltd.
33 *
44 * This program is free software: you can redistribute it and/or modify it under
55 * the terms of the GNU Lesser General Public License version 3, as published by
@@ -14,12 +14,17 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17−#include <QQmlExtensionPlugin>
18−#include <QtQml/qqml.h>
17+#ifndef MOCK_SCREENWINDOW_H
18+#define MOCK_SCREENWINDOW_H
1919
20−class UnityScreensPlugin : public QQmlExtensionPlugin {
20+#include "ScreenWindow.h"
21+
22+class MockScreenWindow : public ScreenWindow
23+{
2124 Q_OBJECT
22− Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface/1.0")
2325public:
24− void registerTypes(const char* uri) override;
26+ explicit MockScreenWindow(QQuickWindow *parent = 0);
27+ ~MockScreenWindow();
2528};
29+
30+#endif // MOCK_SCREENWINDOW_H
tests/mocks/WindowManager/MockScreens.cppnew file+230
@@ -0,0 +1,230 @@
1+/*
2+ * Copyright (C) 2016-2017 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "MockScreens.h"
18+#include "ScreenWindow.h"
19+
20+// qtmirserver
21+#include <qtmir/screen.h>
22+
23+#include <QGuiApplication>
24+#include <QWindow>
25+#include <QWeakPointer>
26+
27+namespace {
28+
29+QWeakPointer<MockScreens> m_screens;
30+
31+class MockScreen : public qtmir::Screen
32+{
33+ Q_OBJECT
34+ Q_PROPERTY(QString outputTypeName READ outputTypeName NOTIFY outputTypeNameChanged)
35+public:
36+ MockScreen()
37+ {
38+ m_sizes.append(new qtmir::ScreenMode(50, QSize(800,568)));
39+ m_sizes.append(new qtmir::ScreenMode(60, QSize(1280,1024)));
40+ m_sizes.append(new qtmir::ScreenMode(60, QSize(1440,900)));
41+ m_sizes.append(new qtmir::ScreenMode(60, QSize(1920,1080)));
42+ m_physicalSize = QSize(800,568);
43+ }
44+ ~MockScreen() {
45+ qDeleteAll(m_sizes);
46+ m_sizes.clear();
47+ }
48+
49+ void connectToWindow(QWindow* w)
50+ {
51+ if (m_connectedWindow == w) return;
52+
53+ if (m_connectedWindow) {
54+ disconnect(m_connectedWindow.data());
55+ }
56+
57+ m_connectedWindow = w;
58+
59+ if (w) {
60+ connect(w, &QWindow::heightChanged, this, [this](int height) {
61+ if (height == 0 || height == m_sizes.first()->size.rheight()) {
62+ return;
63+ }
64+ m_sizes.first()->size.rheight() = height;
65+ Q_EMIT availableModesChanged();
66+
67+ });
68+ connect(w, &QWindow::widthChanged, this, [this](int width) {
69+ if (width == 0 || width == m_sizes.first()->size.rwidth()) {
70+ return;
71+ }
72+ m_sizes.first()->size.rwidth() = width;
73+ Q_EMIT availableModesChanged();
74+ });
75+ connect(w, &QWindow::activeChanged, this, [w, this]() {
76+ if (w->isActive()) setActive(true);
77+ });
78+ if (w->isActive()) setActive(true);
79+
80+ Q_EMIT availableModesChanged();
81+
82+ }
83+ }
84+
85+ miral::DisplayId displayId() const override { return m_id; }
86+ bool used() const override { return m_used; }
87+ QString name() const override { return m_name; }
88+ float scale() const override { return m_scale; }
89+ QSizeF physicalSize() const override { return m_physicalSize; }
90+ qtmir::FormFactor formFactor() const override { return m_formFactor; }
91+ qtmir::OutputTypes outputType() const override { return m_outputType; }
92+ QString outputTypeName() const { return QStringLiteral("Internal"); }
93+ MirPowerMode powerMode() const override { return m_powerMode; }
94+ Qt::ScreenOrientation orientation() const override { return m_orientation; }
95+ QPoint position() const override { return m_position; }
96+ uint currentModeIndex() const override { return m_currentModeIndex; }
97+ bool isActive() const override { return m_active; }
98+
99+ QQmlListProperty<qtmir::ScreenMode> availableModes() override {
100+ return QQmlListProperty<qtmir::ScreenMode>(this, m_sizes);
101+ }
102+
103+ void setActive(bool active) override {
104+ if (m_active != active) {
105+ m_active = active;
106+ Q_EMIT activeChanged(m_active);
107+ }
108+ }
109+
110+ QScreen* qscreen() const override {
111+ if (qGuiApp->topLevelWindows().count() > 0) {
112+ return qGuiApp->topLevelWindows()[0]->screen();
113+ }
114+ return qGuiApp->primaryScreen();
115+ }
116+
117+ qtmir::ScreenConfiguration *beginConfiguration() const override {
118+ auto config = new qtmir::ScreenConfiguration;
119+ config->valid = true;
120+ config->id = m_id.output_id;
121+ config->used = m_used;
122+ config->topLeft = m_position;
123+ config->currentModeIndex = m_currentModeIndex;
124+ config->powerMode = m_powerMode;
125+ config->scale = m_scale;
126+ config->formFactor = m_formFactor;
127+ return config;
128+ }
129+
130+ bool applyConfiguration(qtmir::ScreenConfiguration *configuration) override {
131+ m_used = configuration->used;
132+ m_position = configuration->topLeft;
133+ m_currentModeIndex = configuration->currentModeIndex;
134+ m_powerMode = configuration->powerMode;
135+ m_scale = configuration->scale;
136+ m_formFactor = configuration->formFactor;
137+ return true;
138+ }
139+
140+Q_SIGNALS:
141+ void outputTypeNameChanged();
142+
143+public:
144+ miral::DisplayId m_id;
145+ bool m_active{false};
146+ bool m_used{true};
147+ QString m_name;
148+ qtmir::OutputTypes m_outputType{qtmir::Unknown};
149+ MirPowerMode m_powerMode{mir_power_mode_on};
150+ Qt::ScreenOrientation m_orientation{Qt::PrimaryOrientation};
151+ float m_scale{1.0};
152+ qtmir::FormFactor m_formFactor{qtmir::FormFactorMonitor};
153+ QPoint m_position;
154+ uint m_currentModeIndex{0};
155+ QList<qtmir::ScreenMode*> m_sizes;
156+ QSizeF m_physicalSize;
157+
158+ QPointer<QWindow> m_connectedWindow;
159+};
160+}
161+
162+MockScreens::MockScreens()
163+{
164+ bool ok = false;
165+ int screenCount = qEnvironmentVariableIntValue("UNITY_MOCK_SCREEN_COUNT", &ok);
166+ if (!ok) screenCount = 1;
167+ QPoint lastPoint(0,0);
168+ for (int i = 0; i < screenCount; ++i) {
169+ auto screen = new MockScreen();
170+ screen->m_id.output_id = miral::OutputId{i};
171+ screen->m_active = i == 0;
172+ screen->m_name = QString("Monitor %1").arg(i);
173+ screen->m_position = QPoint(lastPoint.x(), lastPoint.y());
174+ screen->m_currentModeIndex = 0;
175+ m_mocks.append(screen);
176+
177+ lastPoint.rx() += screen->m_sizes[screen->m_currentModeIndex]->size.width();
178+
179+ connect(screen, &qtmir::Screen::activeChanged, this, [=](bool active) {
180+ Q_FOREACH(auto otherScreen, m_mocks) {
181+ if (active && otherScreen != screen) {
182+ otherScreen->setActive(false);
183+ }
184+ }
185+ });
186+ }
187+}
188+
189+MockScreens::~MockScreens()
190+{
191+ qDeleteAll(m_mocks);
192+ m_mocks.clear();
193+}
194+
195+QVector<qtmir::Screen *> MockScreens::screens() const
196+{
197+ return m_mocks;
198+}
199+
200+qtmir::Screen *MockScreens::activeScreen() const
201+{
202+ Q_FOREACH(auto screen, m_mocks) {
203+ if (screen->isActive()) return screen;
204+ }
205+ return nullptr;
206+}
207+
208+QSharedPointer<MockScreens> MockScreens::instance()
209+{
210+ if (!m_screens) {
211+ QSharedPointer<MockScreens> screens(new MockScreens());
212+ m_screens = screens.toWeakRef();
213+ return screens;
214+ } else {
215+ return m_screens.lock();
216+ }
217+}
218+
219+void MockScreens::connectWindow(ScreenWindow *w)
220+{
221+ ConcreteScreen* screen = w->screenWrapper();
222+ if (!screen) return;
223+
224+ auto mockScreen = qobject_cast<MockScreen*>(screen->wrapped());
225+ if (mockScreen) {
226+ mockScreen->connectToWindow(w);
227+ }
228+}
229+
230+#include "MockScreens.moc"
tests/mocks/WindowManager/MockScreens.hnew file+54
@@ -0,0 +1,54 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef MOCK_SCREENS_H
18+#define MOCK_SCREENS_H
19+
20+#include <QAbstractListModel>
21+#include <QSharedPointer>
22+
23+#include <qtmir/screens.h>
24+
25+namespace qtmir
26+{
27+class Screens;
28+}
29+
30+class ConcreteScreen;
31+class ScreenWindow;
32+
33+class MockScreens : public qtmir::Screens
34+{
35+ Q_OBJECT
36+public:
37+ MockScreens();
38+ ~MockScreens();
39+
40+ QVector<qtmir::Screen*> screens() const override;
41+
42+ qtmir::Screen *activeScreen() const override;
43+
44+ static QSharedPointer<MockScreens> instance();
45+
46+public Q_SLOTS:
47+ void connectWindow(ScreenWindow *w);
48+
49+private:
50+ QVector<qtmir::Screen*> m_mocks;
51+};
52+
53+
54+#endif // MOCK_SCREENS_H
tests/mocks/WindowManager/MockScreensConfiguration.cppnew file+47
@@ -0,0 +1,47 @@
1+/*
2+ * Copyright (C) 2017 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+#include "ScreensConfiguration.h"
18+#include "Screen.h"
19+#include "Workspace.h"
20+#include "WorkspaceManager.h"
21+
22+namespace
23+{
24+const int DefaultWorkspacesCount = 2;
25+QHash<QString, int> screensConfig;
26+}
27+
28+ScreensConfiguration::ScreensConfiguration()
29+{
30+}
31+
32+ScreensConfiguration::~ScreensConfiguration()
33+{
34+}
35+
36+void ScreensConfiguration::load(Screen *screen)
37+{
38+ int workspaces = screensConfig.value(screen->name(), DefaultWorkspacesCount);
39+ for (int i = 0; i < workspaces; i++) {
40+ WorkspaceManager::instance()->createWorkspace()->assign(screen->workspaces());
41+ }
42+}
43+
44+void ScreensConfiguration::save(Screen *screen)
45+{
46+ screensConfig[screen->name()] = screen->workspaces()->rowCount();
47+}
tests/mocks/WindowManager/WindowManagementPolicy.cppnew file+128
@@ -0,0 +1,128 @@
1+/*
2+ * Copyright (C) 2017 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+#include "WindowManagementPolicy.h"
18+
19+#include <QDebug>
20+
21+WindowManagementPolicy::WindowManagementPolicy()
22+ : m_dummyWorkspace(std::shared_ptr<miral::Workspace>())
23+{
24+ wmPolicyInterface = this;
25+ m_activeWorkspace = m_dummyWorkspace;
26+}
27+
28+WindowManagementPolicy *WindowManagementPolicy::instance()
29+{
30+ static WindowManagementPolicy* wmPolicy(new WindowManagementPolicy);
31+ return wmPolicy;
32+}
33+
34+std::shared_ptr<miral::Workspace> WindowManagementPolicy::createWorkspace()
35+{
36+ auto workspace = std::make_shared<miral::Workspace>();
37+ m_workspaces.insert(workspace);
38+
39+ if (m_activeWorkspace.lock() == m_dummyWorkspace) {
40+ moveWorkspaceContentToWorkspace(workspace, m_dummyWorkspace);
41+ m_activeWorkspace = workspace;
42+ }
43+ return workspace;
44+}
45+
46+void WindowManagementPolicy::releaseWorkspace(const std::shared_ptr<miral::Workspace> &workspace)
47+{
48+ auto iter = m_workspaces.find(workspace);
49+ if (iter != m_workspaces.end()) m_workspaces.erase(iter);
50+
51+ if (m_workspaces.size() == 0) {
52+ m_activeWorkspace = m_dummyWorkspace;
53+ moveWorkspaceContentToWorkspace(m_dummyWorkspace, workspace);
54+ }
55+}
56+
57+void WindowManagementPolicy::setActiveWorkspace(const std::shared_ptr<miral::Workspace> &workspace)
58+{
59+ if (m_activeWorkspace.lock() == workspace)
60+ return;
61+ m_activeWorkspace = workspace ? workspace : m_dummyWorkspace;
62+}
63+
64+void WindowManagementPolicy::addWindow(const miral::Window &window)
65+{
66+ Q_EMIT windowAdded(window);
67+
68+ auto activeWorkspace = m_activeWorkspace.lock();
69+ if (activeWorkspace) {
70+ m_windows.insert(activeWorkspace, window);
71+
72+ Q_EMIT windowsAddedToWorkspace(activeWorkspace, {window});
73+ }
74+}
75+
76+void WindowManagementPolicy::removeWindow(const miral::Window &window)
77+{
78+ WorkspaceWindows::iterator i = m_windows.begin();
79+ while (i != m_windows.end()) {
80+ if (i.value() == window) {
81+ Q_EMIT windowsAboutToBeRemovedFromWorkspace(i.key(), { window });
82+ i = m_windows.erase(i);
83+ } else {
84+ ++i;
85+ }
86+ }
87+ Q_EMIT windowRemoved(window);
88+}
89+
90+void WindowManagementPolicy::forEachWindowInWorkspace(const std::shared_ptr<miral::Workspace> &workspace, const std::function<void(const miral::Window &)> &callback)
91+{
92+ WorkspaceWindows::iterator i = m_windows.find(workspace);
93+ while (i != m_windows.end() && i.key() == workspace) {
94+ callback(i.value());
95+ ++i;
96+ }
97+}
98+
99+void WindowManagementPolicy::moveWindowToWorkspace(const miral::Window &window, const std::shared_ptr<miral::Workspace> &workspace)
100+{
101+ auto from = m_windows.key(window);
102+ if (from) {
103+ auto iter = m_windows.find(from, window);
104+ Q_EMIT windowsAboutToBeRemovedFromWorkspace(from, { window });
105+ m_windows.erase(iter);
106+ }
107+
108+ m_windows.insert(workspace, window);
109+ Q_EMIT windowsAddedToWorkspace(workspace, { window });
110+}
111+
112+void WindowManagementPolicy::moveWorkspaceContentToWorkspace(const std::shared_ptr<miral::Workspace> &to, const std::shared_ptr<miral::Workspace> &from)
113+{
114+ std::vector<miral::Window> windows;
115+
116+ WorkspaceWindows::iterator i = m_windows.find(from);
117+ while (i != m_windows.end() && i.key() == from) {
118+ windows.push_back(i.value());
119+ ++i;
120+ }
121+ Q_EMIT windowsAboutToBeRemovedFromWorkspace(from, windows);
122+ m_windows.remove(from);
123+
124+ Q_FOREACH(miral::Window window, windows) {
125+ m_windows.insert(to, window);
126+ }
127+ Q_EMIT windowsAddedToWorkspace(to, windows);
128+}
tests/mocks/WindowManager/WindowManagementPolicy.hnew file+75
@@ -0,0 +1,75 @@
1+/*
2+ * Copyright (C) 2017 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+#ifndef MOCKWINDOMANAGEMENTPOLICY_H
18+#define MOCKWINDOMANAGEMENTPOLICY_H
19+
20+#include "wmpolicyinterface.h"
21+
22+#include <QObject>
23+#include <QMultiMap>
24+#include <QVector>
25+
26+#include <miral/window.h>
27+#include <memory>
28+#include <unordered_set>
29+
30+namespace miral {
31+class Workspace {};
32+}
33+
34+// A Fake window management policy for the mock.
35+class Q_DECL_EXPORT WindowManagementPolicy : public QObject,
36+ public WMPolicyInterface
37+{
38+ Q_OBJECT
39+public:
40+ WindowManagementPolicy();
41+
42+ // for use in mocks
43+ static WindowManagementPolicy *instance();
44+
45+ // From WMPolicyInterface
46+ std::shared_ptr<miral::Workspace> createWorkspace() override;
47+ void releaseWorkspace(const std::shared_ptr<miral::Workspace> &workspace) override;
48+ void setActiveWorkspace(const std::shared_ptr<miral::Workspace> &workspace) override;
49+
50+ void addWindow(const miral::Window& window);
51+ void removeWindow(const miral::Window& window);
52+
53+ void forEachWindowInWorkspace(std::shared_ptr<miral::Workspace> const &workspace,
54+ std::function<void(miral::Window const&)> const &callback);
55+
56+ void moveWindowToWorkspace(const miral::Window &window, const std::shared_ptr<miral::Workspace> &workspace);
57+
58+ void moveWorkspaceContentToWorkspace(const std::shared_ptr<miral::Workspace> &to,
59+ const std::shared_ptr<miral::Workspace> &from);
60+
61+Q_SIGNALS:
62+ void windowAdded(const miral::Window& window);
63+ void windowRemoved(const miral::Window& window);
64+ void windowsAddedToWorkspace(const std::shared_ptr<miral::Workspace> &workspace, const std::vector<miral::Window> &windows);
65+ void windowsAboutToBeRemovedFromWorkspace(const std::shared_ptr<miral::Workspace> &workspace, const std::vector<miral::Window> &windows);
66+
67+private:
68+ std::weak_ptr<miral::Workspace> m_activeWorkspace;
69+ std::shared_ptr<miral::Workspace> m_dummyWorkspace;
70+ std::unordered_set<std::shared_ptr<miral::Workspace>> m_workspaces;
71+
72+ typedef QMultiMap<std::shared_ptr<miral::Workspace>, miral::Window> WorkspaceWindows;
73+ WorkspaceWindows m_windows;
74+};
75+#endif // UNITY_WINDOWMANAGEMENTPOLICY_H
tests/mocks/WindowManager/WindowManagerPlugin.cppnew file+89
@@ -0,0 +1,89 @@
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+#include "WindowManagerPlugin.h"
18+
19+#include "AvailableDesktopArea.h"
20+#include "MockScreens.h"
21+#include "MockScreenWindow.h"
22+#include "Screen.h"
23+#include "ScreenAttached.h"
24+#include "Screens.h"
25+#include "ScreensConfiguration.h"
26+#include "TopLevelWindowModel.h"
27+#include "Window.h"
28+#include "WindowMargins.h"
29+#include "WindowManagementPolicy.h"
30+#include "WindowManagerObjects.h"
31+#include "WorkspaceManager.h"
32+#include "Workspace.h"
33+#include "WorkspaceModel.h"
34+
35+#include <QtQml>
36+
37+static const QString notInstantiatable = QStringLiteral("Not instantiatable");
38+
39+static QObject *workspace_manager(QQmlEngine *engine, QJSEngine *scriptEngine)
40+{
41+ Q_UNUSED(engine)
42+ Q_UNUSED(scriptEngine)
43+ return WorkspaceManager::instance();
44+}
45+QObject* screensSingleton(QQmlEngine* engine, QJSEngine* scriptEngine) {
46+ Q_UNUSED(engine);
47+ Q_UNUSED(scriptEngine);
48+ return ConcreteScreens::self();
49+}
50+QObject* objectsSingleton(QQmlEngine* engine, QJSEngine* scriptEngine) {
51+ Q_UNUSED(engine);
52+ Q_UNUSED(scriptEngine);
53+ return WindowManagerObjects::instance();
54+}
55+
56+void WindowManagerPlugin::registerTypes(const char *uri)
57+{
58+ qmlRegisterType<AvailableDesktopArea>(uri, 1, 0, "AvailableDesktopArea");
59+ qmlRegisterType<WindowMargins>(uri, 1, 0, "WindowMargins");
60+ qmlRegisterSingletonType<WorkspaceManager>(uri, 1, 0, "WorkspaceManager", workspace_manager);
61+ qmlRegisterSingletonType<ConcreteScreens>(uri, 1, 0, "Screens", screensSingleton);
62+ qmlRegisterUncreatableType<qtmir::ScreenMode>(uri, 1, 0, "ScreenMode", notInstantiatable);
63+ qmlRegisterSingletonType<WindowManagerObjects>(uri, 1, 0, "WindowManagerObjects", objectsSingleton);
64+
65+ qRegisterMetaType<ConcreteScreen*>("ConcreteScreen*");
66+ qRegisterMetaType<ProxyScreens*>("ProxyScreens*");
67+ qRegisterMetaType<Workspace*>("Workspace*");
68+ qRegisterMetaType<TopLevelWindowModel*>("TopLevelWindowModel*");
69+ qRegisterMetaType<ScreenConfig*>("ScreenConfig*");
70+ qRegisterMetaType<WorkspaceModel*>("WorkspaceModel*");
71+
72+ qRegisterMetaType<Window*>("Window*");
73+ qRegisterMetaType<QAbstractListModel*>("QAbstractListModel*");
74+
75+ qmlRegisterType<MockScreenWindow>(uri, 1, 0, "ScreenWindow");
76+ qmlRegisterRevision<QWindow,1>(uri, 1, 0);
77+
78+ qmlRegisterUncreatableType<WMScreen>(uri, 1, 0, "WMScreen", notInstantiatable);
79+}
80+
81+void WindowManagerPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
82+{
83+ QQmlExtensionPlugin::initializeEngine(engine, uri);
84+
85+ // Make sure we've initialized the wm policy.
86+ WindowManagementPolicy::instance();
87+ // Create Screens
88+ new ConcreteScreens(MockScreens::instance(), new ScreensConfiguration());
89+}
tests/mocks/WindowManager/WindowManagerPlugin.hnew file+33
@@ -0,0 +1,33 @@
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+#ifndef WINDOWMANAGER_PLUGIN_H
18+#define WINDOWMANAGER_PLUGIN_H
19+
20+#include <QtQml/QQmlEngine>
21+#include <QtQml/QQmlExtensionPlugin>
22+
23+class WindowManagerPlugin : public QQmlExtensionPlugin
24+{
25+ Q_OBJECT
26+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
27+
28+public:
29+ void registerTypes(const char *uri) override;
30+ void initializeEngine(QQmlEngine *engine, const char *uri) override;
31+};
32+
33+#endif // WINDOWMANAGER_PLUGIN_H
tests/mocks/WindowManager/qmldirnew file+2
@@ -0,0 +1,2 @@
1+module WindowManager
2+plugin MockWindowManager-qml
tests/plugins/GlobalShortcut/GlobalShortcutTest.cpp+1
@@ -38,6 +38,7 @@ private Q_SLOTS:
3838 m_inactiveShortcut = dynamic_cast<GlobalShortcut*>(m_view->rootObject()->property("inactiveShortcut").value<QObject*>());
3939 QVERIFY(m_inactiveShortcut);
4040 m_view->show();
41+ m_view->requestActivate();
4142 QTest::qWaitForWindowExposed(m_view);
4243 }
4344
tests/plugins/WindowManager/CMakeLists.txt+5−1
@@ -2,6 +2,7 @@ include_directories(
22 ${CMAKE_SOURCE_DIR}/plugins/WindowManager
33 ${CMAKE_CURRENT_BINARY_DIR}
44 ${CMAKE_SOURCE_DIR}/tests/mocks
5+ ${libunity8-private_SOURCE_DIR}
56 )
67
78add_executable(TopLevelWindowModelTestExec
@@ -19,7 +20,10 @@ add_executable(TopLevelWindowModelTestExec
1920 )
2021qt5_use_modules(TopLevelWindowModelTestExec Test Core Gui Qml Quick)
2122
22−target_link_libraries(TopLevelWindowModelTestExec windowmanager-qml)
23+target_link_libraries(TopLevelWindowModelTestExec
24+ windowmanager-qml
25+ unity8-private
26+ )
2327
2428install(TARGETS TopLevelWindowModelTestExec
2529 DESTINATION "${SHELL_PRIVATE_LIBDIR}/tests/plugins/WindowManager"
tests/plugins/WindowManager/UnityApplicationMocks.h+25
@@ -25,6 +25,8 @@
2525// from tests/mocks
2626#include <Unity/Application/MirSurfaceListModel.h>
2727
28+#include "wmpolicyinterface.h"
29+
2830using namespace unity::shell::application;
2931
3032class MirSurface : public MirSurfaceInterface
@@ -88,6 +90,13 @@ class SurfaceManager : public SurfaceManagerInterface
8890 Q_OBJECT
8991
9092public:
93+ void forEachSurfaceInWorkspace(const std::shared_ptr<miral::Workspace>&,
94+ const std::function<void(unity::shell::application::MirSurfaceInterface*)>&) override {}
95+ void moveSurfaceToWorkspace(unity::shell::application::MirSurfaceInterface*,
96+ const std::shared_ptr<miral::Workspace>&) override {}
97+ void moveWorkspaceContentToWorkspace(const std::shared_ptr<miral::Workspace>&,
98+ const std::shared_ptr<miral::Workspace>&) override {}
99+
91100 void raise(MirSurfaceInterface *) override {}
92101 void activate(MirSurfaceInterface *) override {}
93102};
@@ -208,4 +217,20 @@ private:
208217 QList<Application*> m_applications;
209218};
210219
220+namespace miral {
221+class Workspace {};
222+}
223+
224+class WindowManagementPolicy : public WMPolicyInterface
225+{
226+public:
227+ WindowManagementPolicy() {}
228+
229+ std::shared_ptr<miral::Workspace> createWorkspace() override { return std::make_shared<miral::Workspace>(); }
230+
231+ void releaseWorkspace(const std::shared_ptr<miral::Workspace>&) override {}
232+
233+ void setActiveWorkspace(const std::shared_ptr<miral::Workspace>&) override {}
234+};
235+
211236#endif // UNITYAPPLICATIONMOCKS_H
tests/plugins/WindowManager/tst_TopLevelWindowModel.cpp+20−8
@@ -19,8 +19,13 @@
1919// WindowManager plugin
2020#include <TopLevelWindowModel.h>
2121#include <Window.h>
22+#include <WindowManagerObjects.h>
23+#include <Workspace.h>
24+#include <WorkspaceManager.h>
2225
2326#include "UnityApplicationMocks.h"
27+#include "wmpolicyinterface.h"
28+
2429
2530class tst_TopLevelWindowModel : public QObject
2631{
@@ -34,6 +39,7 @@ private Q_SLOTS:
3439 void secondSurfaceIsHidden();
3540
3641private:
42+ Workspace* workspace{nullptr};
3743 ApplicationManager *applicationManager{nullptr};
3844 SurfaceManager *surfaceManager{nullptr};
3945 TopLevelWindowModel *topLevelWindowModel{nullptr};
@@ -41,24 +47,30 @@ private:
4147
4248void tst_TopLevelWindowModel::init()
4349{
50+ wmPolicyInterface = new WindowManagementPolicy();
51+
4452 applicationManager = new ApplicationManager;
4553 surfaceManager = new SurfaceManager;
54+ WindowManagerObjects::instance()->setApplicationManager(applicationManager);
55+ WindowManagerObjects::instance()->setSurfaceManager(surfaceManager);
4656
47− topLevelWindowModel = new TopLevelWindowModel;
48− topLevelWindowModel->setApplicationManager(applicationManager);
49− topLevelWindowModel->setSurfaceManager(surfaceManager);
57+ workspace = WorkspaceManager::instance()->createWorkspace();
58+ topLevelWindowModel = workspace->windowModel();
5059}
5160
5261void tst_TopLevelWindowModel::cleanup()
5362{
54− delete topLevelWindowModel;
55− topLevelWindowModel = nullptr;
63+ delete workspace;
64+ workspace = nullptr;
5665
5766 delete surfaceManager;
5867 surfaceManager = nullptr;
5968
6069 delete applicationManager;
6170 applicationManager = nullptr;
71+
72+ delete wmPolicyInterface;
73+ wmPolicyInterface = nullptr;
6274}
6375
6476void tst_TopLevelWindowModel::singleSurfaceStartsHidden()
@@ -73,7 +85,7 @@ void tst_TopLevelWindowModel::singleSurfaceStartsHidden()
7385 auto surface = new MirSurface;
7486 surface->m_state = Mir::HiddenState;
7587 application->m_surfaceList.addSurface(surface);
76− Q_EMIT surfaceManager->surfaceCreated(surface);
88+ Q_EMIT surfaceManager->surfacesAddedToWorkspace(workspace->workspace(), {surface});
7789
7890 QCOMPARE(topLevelWindowModel->rowCount(), 1);
7991 // not showing the surface as it's still hidden
@@ -97,7 +109,7 @@ void tst_TopLevelWindowModel::secondSurfaceIsHidden()
97109
98110 auto firstSurface = new MirSurface;
99111 application->m_surfaceList.addSurface(firstSurface);
100− Q_EMIT surfaceManager->surfaceCreated(firstSurface);
112+ Q_EMIT surfaceManager->surfacesAddedToWorkspace(workspace->workspace(), {firstSurface});
101113
102114 QCOMPARE(topLevelWindowModel->rowCount(), 1);
103115 QCOMPARE((void*)topLevelWindowModel->windowAt(0)->surface(), (void*)firstSurface);
@@ -105,7 +117,7 @@ void tst_TopLevelWindowModel::secondSurfaceIsHidden()
105117 auto secondSurface = new MirSurface;
106118 secondSurface->m_state = Mir::HiddenState;
107119 application->m_surfaceList.addSurface(secondSurface);
108− Q_EMIT surfaceManager->surfaceCreated(secondSurface);
120+ Q_EMIT surfaceManager->surfacesAddedToWorkspace(workspace->workspace(), {secondSurface});
109121
110122 // still only the first surface is exposed by TopLevelWindowModel
111123 QCOMPARE(topLevelWindowModel->rowCount(), 1);
tests/qmltests/ApplicationMenuDataLoader.qml+3−6
@@ -22,19 +22,16 @@ import Unity.Indicators 0.1 as Indicators
2222
2323Object {
2424
25− property alias surfaceManager: sMgrHandler.target
26−
2725 Connections {
28− id: sMgrHandler
29− target: null
26+ target: SurfaceManager
3027 onSurfaceCreated: {
3128 var fakeMenuPath = "/" + surface.persistentId.replace(/\W+/g, "");
3229
3330 ApplicationMenuRegistry.RegisterSurfaceMenu(surface.persistentId, fakeMenuPath, fakeMenuPath, ":1");
3431 Indicators.UnityMenuModelCache.setCachedModelData(fakeMenuPath, generateTestData(4, 3, 2, 3, "menu"));
3532 }
36− onSurfaceDestroyed: {
37− ApplicationMenuRegistry.UnregisterSurfaceMenu(persistentSurfaceId, "/app");
33+ onSurfaceRemoved: {
34+ ApplicationMenuRegistry.UnregisterSurfaceMenu(surface.persistentId, "/app");
3835 }
3936 }
4037
tests/qmltests/ApplicationMenus/tst_MenuBar.qml+2−2
@@ -24,6 +24,7 @@ import Unity.Test 0.1
2424import Utils 0.1
2525
2626import "../../../qml/ApplicationMenus"
27+import "../../../qml/Components/PanelState"
2728import ".."
2829
2930Item {
@@ -42,10 +43,8 @@ Item {
4243 value: false
4344 }
4445
45− SurfaceManager { id: sMgr }
4646 ApplicationMenuDataLoader {
4747 id: appMenuData
48− surfaceManager: sMgr
4948 }
5049
5150 Rectangle {
@@ -67,6 +66,7 @@ Item {
6766 id: menuBackend
6867 modelData: null
6968 }
69+ panelState: PanelState {}
7070 }
7171 }
7272
tests/qmltests/ApplicationMenus/tst_MenuPopup.qml+2
@@ -24,6 +24,7 @@ import Unity.Test 0.1
2424import Utils 0.1
2525
2626import "../../../qml/ApplicationMenus"
27+import "../../../qml/Components/PanelState"
2728import ".."
2829
2930Item {
@@ -59,6 +60,7 @@ Item {
5960 }}
6061 ]
6162 }
63+ panelState: PanelState {}
6264 }
6365 }
6466
tests/qmltests/CMakeLists.txt+1
@@ -5,6 +5,7 @@ add_unity8_qmltest(. OrientedShell)
55add_unity8_qmltest(. DisabledScreenNotice)
66add_unity8_qmltest(. Shell)
77add_unity8_qmltest(. ShellWithPin)
8+add_unity8_qmltest(. ShellApplication)
89add_unity8_qmltest(. DeviceConfiguration)
910add_unity8_qmltest_data(. EdgeBarrierControls)
1011add_unity8_qmltest_data(. ApplicationMenuDataLoader)
tests/qmltests/Components/tst_VirtualTouchPad.qml+2−2
@@ -39,11 +39,11 @@ Rectangle {
3939
4040 SignalSpy {
4141 id: mouseEventSpy1
42− target: touchScreenPad.uinput
42+ target: UInput
4343 }
4444 SignalSpy {
4545 id: mouseEventSpy2
46− target: touchScreenPad.uinput
46+ target: UInput
4747 }
4848
4949 UnityTestCase {
tests/qmltests/Panel/tst_ActiveCallHint.qml−2
@@ -41,8 +41,6 @@ Item {
4141 elapsedTimerRunning: true
4242 }
4343
44− SurfaceManager {}
45−
4644 ActiveCallHint {
4745 id: callHint
4846 anchors {
tests/qmltests/Panel/tst_Panel.qml+39−15
@@ -42,10 +42,10 @@ PanelTest {
4242 value: keyboardAttached.checked
4343 }
4444
45− SurfaceManager { id: sMgr }
45+ readonly property alias panelState: panel.panelState
46+
4647 ApplicationMenuDataLoader {
4748 id: appMenuData
48− surfaceManager: sMgr
4949 }
5050
5151 Component.onCompleted: {
@@ -99,6 +99,8 @@ PanelTest {
9999 model: root.indicatorsModel
100100 hides: [ panel.applicationMenus ]
101101 }
102+
103+ panelState: PanelState {}
102104 }
103105 }
104106 }
@@ -153,7 +155,7 @@ PanelTest {
153155 Layout.fillWidth: true
154156 CheckBox {
155157 id: windowControlsCB
156− onClicked: PanelState.decorationsVisible = checked
158+ onClicked: panelState.decorationsVisible = checked
157159 }
158160 Label {
159161 text: "Show window decorations"
@@ -164,7 +166,7 @@ PanelTest {
164166 RowLayout {
165167 Layout.fillWidth: true
166168 CheckBox {
167− onClicked: PanelState.title = checked ? "Fake window title" : ""
169+ onClicked: panelState.title = checked ? "Fake window title" : ""
168170 }
169171 Label {
170172 text: "Show fake window title"
@@ -250,7 +252,7 @@ PanelTest {
250252
251253 SignalSpy {
252254 id: windowControlButtonsSpy
253− target: PanelState
255+ target: panelState
254256 signalName: "closeClicked"
255257 }
256258
@@ -260,8 +262,8 @@ PanelTest {
260262 panel.fullscreenMode = false;
261263 callManager.foregroundCall = null;
262264
263− PanelState.title = "";
264− PanelState.decorationsVisible = false;
265+ panelState.title = "";
266+ panelState.decorationsVisible = false;
265267
266268 // Put the mouse somewhere neutral so it doesn't hover over things
267269 // and mess up the test
@@ -462,7 +464,7 @@ PanelTest {
462464 }
463465
464466 function test_hint(data) {
465− PanelState.title = "Fake Title"
467+ panelState.title = "Fake Title"
466468 panel.fullscreenMode = data.fullscreen;
467469 callManager.foregroundCall = data.call;
468470
@@ -504,7 +506,7 @@ PanelTest {
504506 // menus, first by running the hint animation, then after dragging down will
505507 // expose more of the panel. Releasing the touch will complete the show.
506508 function test_drag_applicationMenu_down_shows_menu(data) {
507− PanelState.title = "Fake Title";
509+ panelState.title = "Fake Title";
508510 panel.fullscreenMode = data.fullscreen;
509511 callManager.foregroundCall = data.call;
510512
@@ -596,7 +598,7 @@ PanelTest {
596598 }
597599
598600 function test_darkenedAreaEatsAllApplicationMenuEvents() {
599− PanelState.title = "Fake Title"
601+ panelState.title = "Fake Title"
600602
601603 // The center of the area not covered by the indicators menu
602604 // Ie, the visible darkened area behind the menu
@@ -717,7 +719,7 @@ PanelTest {
717719 var windowControlArea = findChild(panel, "windowControlArea");
718720 verify(windowControlArea, "Window control area should have been created in windowed mode")
719721
720− PanelState.decorationsVisible = true;
722+ panelState.decorationsVisible = true;
721723 // click in very topleft corner and verify the close button got clicked too
722724 mouseMove(panel, 0, 0);
723725 mouseClick(panel, 0, 0, undefined /*button*/, undefined /*modifiers*/, 100 /*short delay*/);
@@ -779,8 +781,26 @@ PanelTest {
779781 }
780782 }
781783
782− function test_windowedApplicationMenuBarShowOnMouseHover() {
783− PanelState.title = "Fake Title";
784+ function test_stagedApplicationMenuBarShowOnMouseHover() {
785+ panelState.title = "Fake Title";
786+ panel.mode = "staged";
787+ mouseEmulation.checked = false;
788+
789+ var appTitle = findChild(panel, "panelTitle"); verify(appTitle);
790+ var appMenuRow = findChild(panel.applicationMenus, "panelRow"); verify(appMenuRow);
791+ var appMenuBar = findChild(panel, "menuBar"); verify(appMenuBar);
792+
793+ tryCompare(appTitle, "visible", true, undefined, "App title should be visible");
794+ tryCompare(appMenuBar, "visible", false, undefined, "App menu bar should not be visible");
795+
796+ mouseMove(panel, panel.width/2, panel.panelHeight);
797+
798+ tryCompare(appTitle, "visible", false, undefined, "App title should not be visible on mouse hover");
799+ tryCompare(appMenuBar, "visible", true, undefined, "App menu bar should be visible on mouse hover");
800+ }
801+
802+ function test_windowedApplicationMenuShowOnMouseHoverWhenDecorationsShown() {
803+ panelState.title = "Fake Title";
784804 panel.mode = "windowed";
785805 mouseEmulation.checked = false;
786806
@@ -794,6 +814,10 @@ PanelTest {
794814 mouseMove(panel, panel.width/2, panel.panelHeight);
795815
796816 waitForRendering(panel);
817+ tryCompare(appTitle, "visible", true, undefined, "App title should still be visible on mouse hover when panel decorations are not visible");
818+ tryCompare(appMenuBar, "visible", false, undefined, "App menu bar should be visible on mouse hover when panel decorations are not visible");
819+
820+ panelState.decorationsVisible = true;
797821
798822 var appMenuBarLoader = findChild(panel, "menuBarLoader")
799823 tryVerify(function() {return appMenuBarLoader.item});
@@ -844,7 +868,7 @@ PanelTest {
844868
845869 var indicatorsBar = findChild(panel.applicationMenus, "indicatorsBar");
846870
847− PanelState.title = "Fake Title";
871+ panelState.title = "Fake Title"
848872 waitForRendering(panel);
849873 pullDownApplicationsMenu(0 /*xPos*/);
850874 compare(aboutToShowCalledSpy.count, 1);
@@ -878,7 +902,7 @@ PanelTest {
878902
879903 var indicatorsBar = findChild(panel.applicationMenus, "indicatorsBar");
880904
881− PanelState.title = "Fake Title"
905+ panelState.title = "Fake Title"
882906 pullDownApplicationsMenu(0 /*xPos*/);
883907
884908 tryCompare(indicatorsBar, "currentItemIndex", 0);
tests/qmltests/Stage/tst_ApplicationWindow.qml+10−44
@@ -37,8 +37,6 @@ Rectangle {
3737 }
3838 property QtObject fakeApplication: null
3939
40− SurfaceManager{}
41−
4240 Loader {
4341 id: applicationWindowLoader
4442 focus: true
@@ -247,13 +245,12 @@ Rectangle {
247245 function test_showSplashUntilAppFullyInit_data() {
248246 return [
249247 {tag: "state=Running then create surface", swapInitOrder: false},
250−
251248 {tag: "create surface then state=Running", swapInitOrder: true},
252249 ]
253250 }
254251
255252 function test_showSplashUntilAppFullyInit() {
256− verify(stateGroup.state === "splashScreen");
253+ verify(stateGroup.state === "splash");
257254
258255 if (data.swapInitOrder) {
259256 surfaceCheckbox.checked = true;
@@ -261,7 +258,7 @@ Rectangle {
261258 setApplicationState(appRunning);
262259 }
263260
264− verify(stateGroup.state === "splashScreen");
261+ verify(stateGroup.state === "splash");
265262
266263 if (data.swapInitOrder) {
267264 setApplicationState(appRunning);
@@ -286,27 +283,8 @@ Rectangle {
286283 waitUntilTransitionsEnd(stateGroup);
287284 }
288285
289− function test_killedAppShowsScreenshot() {
290− surfaceCheckbox.checked = true;
291− setApplicationState(appRunning);
292− tryCompare(stateGroup, "state", "surface");
293−
294− setApplicationState(appSuspended);
295−
296− verify(stateGroup.state === "surface");
297− verify(fakeApplication.surface !== null);
298−
299− // kill it!
300− surfaceCheckbox.checked = false;
301− setApplicationState(appStopped);
302−
303− tryCompare(stateGroup, "state", "screenshot");
304− tryCompare(fakeApplication.surfaceList, "count", 0);
305− }
306−
307286 function test_restartApp() {
308− var screenshotImage = findChild(applicationWindow, "screenshotImage");
309−
287+ tryCompare(stateGroup, "state", "splash");
310288 surfaceCheckbox.checked = true;
311289 setApplicationState(appRunning);
312290 tryCompare(stateGroup, "state", "surface");
@@ -318,26 +296,23 @@ Rectangle {
318296 surfaceCheckbox.checked = false;
319297 setApplicationState(appStopped);
320298
321− tryCompare(stateGroup, "state", "screenshot");
322299 waitUntilTransitionsEnd(stateGroup);
323− tryCompare(applicationWindow, "surface", null);
300+ tryCompare(stateGroup, "state", "surface");
324301
325302 // and restart it
326303 setApplicationState(appStarting);
327304
328305 waitUntilTransitionsEnd(stateGroup);
329− verify(stateGroup.state === "screenshot");
330− verify(applicationWindow.surface === null);
306+ verify(stateGroup.state === "surface");
331307
332308 setApplicationState(appRunning);
333309
334310 waitUntilTransitionsEnd(stateGroup);
335− verify(stateGroup.state === "screenshot");
311+ verify(stateGroup.state === "surface");
336312
337313 surfaceCheckbox.checked = true;
338314
339315 tryCompare(stateGroup, "state", "surface");
340− tryCompare(screenshotImage, "status", Image.Null);
341316 }
342317
343318 function test_appCrashed() {
@@ -345,13 +320,15 @@ Rectangle {
345320 setApplicationState(appRunning);
346321 tryCompare(stateGroup, "state", "surface");
347322 waitUntilTransitionsEnd(stateGroup);
323+ var surface = applicationWindow.surface;
348324
349325 // oh, it crashed...
350326 surfaceCheckbox.checked = false;
351327 setApplicationState(appStopped);
352328
353− tryCompare(stateGroup, "state", "screenshot");
354− tryCompare(applicationWindow, "surface", null);
329+ waitUntilTransitionsEnd(stateGroup);
330+ tryCompare(stateGroup, "state", "surface");
331+ tryCompare(applicationWindow, "surface", surface);
355332 }
356333
357334 function test_keepSurfaceWhileInvisible() {
@@ -395,17 +372,6 @@ Rectangle {
395372 verify(surfaceItem.touchReleaseCount === 1);
396373 }
397374
398− function test_showNothingOnSuddenSurfaceLoss() {
399− surfaceCheckbox.checked = true;
400− setApplicationState(appRunning);
401− tryCompare(stateGroup, "state", "surface");
402− waitUntilTransitionsEnd(stateGroup);
403−
404− applicationWindow.surface = null;
405−
406− tryCompare(stateGroup, "state", "void");
407− }
408−
409375 function test_surfaceActiveFocusFollowsAppWindowInterative() {
410376 applicationWindow.interactive = false;
411377 applicationWindow.interactive = true;
tests/qmltests/Stage/tst_DecoratedWindow.qml+5−3
@@ -46,10 +46,12 @@ Rectangle {
4646 value: false
4747 }
4848
49− SurfaceManager { id: sMgr }
5049 ApplicationMenuDataLoader {
5150 id: appMenuData
52− surfaceManager: sMgr
51+ }
52+
53+ PanelState {
54+ id: panelState
5355 }
5456
5557 Item {
@@ -99,7 +101,7 @@ Rectangle {
99101 surface: fakeApplication && fakeApplication.surfaceList.count > 0 ? fakeApplication.surfaceList.get(0) : null
100102
101103 Binding {
102− target: PanelState
104+ target: panelState
103105 property: "focusedPersistentSurfaceId"
104106 value: decoratedWindow.surface ? decoratedWindow.surface.persistentId : "x"
105107 }
tests/qmltests/Stage/tst_DesktopStage.qml+7−13
@@ -63,17 +63,11 @@ Item {
6363 }
6464 }
6565
66− SurfaceManager { id: sMgr }
6766 ApplicationMenuDataLoader {
6867 id: appMenuData
69− surfaceManager: sMgr
7068 }
7169
72− TopLevelWindowModel {
73− id: topSurfaceList
74− applicationManager: ApplicationManager
75− surfaceManager: sMgr
76− }
70+ readonly property var topSurfaceList: WorkspaceManager.activeWorkspace.windowModel
7771
7872 Loader {
7973 id: stageLoader
@@ -103,11 +97,12 @@ Item {
10397 availableDesktopArea: availableDesktopAreaItem
10498 interactive: true
10599 mode: "windowed"
100+ panelState: PanelState {}
106101
107102 Item {
108103 id: availableDesktopAreaItem
109104 anchors.fill: parent
110− anchors.topMargin: PanelState.panelHeight
105+ anchors.topMargin: parent.panelState.panelHeight
111106 }
112107 }
113108 }
@@ -180,6 +175,7 @@ Item {
180175
181176 stage: stageLoader.status === Loader.Ready ? stageLoader.item : null
182177 topLevelSurfaceList: topSurfaceList
178+ property var panelState: stage ? stage.panelState : null
183179
184180 function init() {
185181 // wait until unity8-dash is up and running.
@@ -562,8 +558,6 @@ Item {
562558 var gmailDelegate = startApplication("gmail-webapp");
563559 verify(gmailDelegate);
564560
565− wait(2000)
566−
567561 var gmailMaximizeButton = findChild(gmailDelegate, "maximizeWindowButton");
568562 verify(gmailMaximizeButton);
569563 mouseClick(gmailMaximizeButton);
@@ -638,19 +632,19 @@ Item {
638632 maximizeDelegate(facebookAppDelegate);
639633
640634 // verify the drop shadow is still not visible
641− verify(PanelState.dropShadow == false);
635+ verify(panelState.dropShadow == false);
642636
643637 // start a foreground app, not maximized
644638 var dialerAppDelegate = startApplication("dialer-app");
645639
646640 // verify the drop shadow becomes visible
647− tryCompareFunction(function() { return PanelState.dropShadow; }, true);
641+ tryCompareFunction(function() { return panelState.dropShadow; }, true);
648642
649643 // close the maximized app
650644 ApplicationManager.stopApplication("facebook-webapp");
651645
652646 // verify the drop shadow is gone
653− tryCompare(PanelState, "dropShadow", false);
647+ tryCompare(panelState, "dropShadow", false);
654648 }
655649
656650 function test_threeFingerTapShowsWindowControls_data() {
tests/qmltests/Stage/tst_PhoneStage.qml+7−8
@@ -19,6 +19,7 @@ import QtTest 1.0
1919import Unity.Test 0.1 as UT
2020import ".."
2121import "../../../qml/Components"
22+import "../../../qml/Components/PanelState"
2223import "../../../qml/Stage"
2324import Ubuntu.Components 1.3
2425import Unity.Application 0.1
@@ -31,10 +32,10 @@ Item {
3132
3233 property var greeter: { fullyShown: true }
3334
34− SurfaceManager { id: sMgr }
35+ readonly property var topLevelSurfaceList: WorkspaceManager.activeWorkspace.windowModel
36+
3537 ApplicationMenuDataLoader {
3638 id: appMenuData
37− surfaceManager: sMgr
3839 }
3940
4041 Stage {
@@ -47,19 +48,17 @@ Item {
4748 orientations: Orientations {}
4849 applicationManager: ApplicationManager
4950 mode: "staged"
50− topLevelSurfaceList: TopLevelWindowModel {
51− id: topLevelSurfaceList
52− applicationManager: ApplicationManager
53− surfaceManager: sMgr
54− }
51+ topLevelSurfaceList: root.topLevelSurfaceList
5552 availableDesktopArea: availableDesktopAreaItem
5653 Item {
5754 id: availableDesktopAreaItem
5855 anchors.fill: parent
5956 }
57+
6058 Component.onCompleted: {
6159 ApplicationManager.startApplication("unity8-dash");
6260 }
61+ panelState: PanelState {}
6362 }
6463
6564 Flickable {
@@ -305,7 +304,6 @@ Item {
305304
306305 performEdgeSwipeToShowAppSpread();
307306
308− print("tapping", selectedAppDeleage.appId, selectedAppDeleage.visible)
309307 if (selectedAppDeleage.x > stage.width - units.gu(5)) {
310308 touchFlick(stage, stage.width - units.gu(2), stage.height / 2, units.gu(2), stage.height / 2, true, true, units.gu(2), 10)
311309 }
@@ -534,6 +532,7 @@ Item {
534532 performEdgeSwipeToShowAppSpread();
535533
536534 var appDelegate = findChild(stage, "appDelegate_" + webbrowserSurfaceId);
535+ verify(appDelegate);
537536 var dragArea = findChild(appDelegate, "dragArea");
538537 verify(dragArea);
539538 tryCompare(dragArea, "closeable", true);
tests/qmltests/Stage/tst_SurfaceContainer.qml−2
@@ -51,8 +51,6 @@ Rectangle {
5151 }
5252 }
5353
54− SurfaceManager {}
55−
5654 Loader {
5755 id: surfaceContainerLoader
5856 focus: true
tests/qmltests/Stage/tst_TabletStage.qml+65−22
@@ -26,6 +26,7 @@ import WindowManager 1.0
2626import ".."
2727import "../../../qml/Stage"
2828import "../../../qml/Components"
29+import "../../../qml/Components/PanelState"
2930
3031Rectangle {
3132 id: root
@@ -35,10 +36,10 @@ Rectangle {
3536
3637 property var greeter: { fullyShown: true }
3738
38− SurfaceManager { id: sMgr }
39+ readonly property var topLevelSurfaceList: WorkspaceManager.activeWorkspace.windowModel
40+
3941 ApplicationMenuDataLoader {
4042 id: appMenuData
41− surfaceManager: sMgr
4243 }
4344
4445 Stage {
@@ -56,11 +57,8 @@ Rectangle {
5657 focus: true
5758 mode: "stagedWithSideStage"
5859 applicationManager: ApplicationManager
59− topLevelSurfaceList: TopLevelWindowModel {
60− id: topLevelSurfaceList
61− applicationManager: ApplicationManager
62− surfaceManager: sMgr
63− }
60+ topLevelSurfaceList: root.topLevelSurfaceList
61+ panelState: PanelState {}
6462 availableDesktopArea: availableDesktopAreaItem
6563 Item {
6664 id: availableDesktopAreaItem
@@ -151,7 +149,6 @@ Rectangle {
151149 name: "TabletStage"
152150 when: windowShown
153151
154− readonly property alias topSurfaceList: stage.topLevelSurfaceList
155152 property Item sideStage: stage ? findChild(stage, "sideStage") : null
156153
157154 function init() {
@@ -165,6 +162,7 @@ Rectangle {
165162
166163 sideStage.hideNow()
167164 tryCompare(sideStage, "x", stage.width)
165+
168166 }
169167
170168 function cleanup() {
@@ -226,7 +224,7 @@ Rectangle {
226224 tryCompare(stage, "state", "spread");
227225 }
228226
229− function swipeSurfaceUpwards(surfaceId) {
227+ function swipeSurfaceDownwards(surfaceId) {
230228 var appWindow = findAppWindowForSurfaceId(surfaceId);
231229 verify(appWindow);
232230
@@ -234,7 +232,7 @@ Rectangle {
234232 // to not be covered by other surfaces when they're all being shown in the spread
235233 touchFlick(appWindow,
236234 appWindow.width * 0.1, appWindow.height / 2,
237− appWindow.width * 0.1, -appWindow.height / 2);
235+ appWindow.width * 0.1, appWindow.height * 1.5);
238236 }
239237
240238 function dragToSideStage(surfaceId) {
@@ -308,6 +306,8 @@ Rectangle {
308306 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
309307
310308 var webbrowserSurfaceId = launchApp("morph");
309+ webbrowserCheckBox.checked = true;
310+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
311311 var webbrowserDelegate = findChild(stage, "appDelegate_" + webbrowserSurfaceId);
312312 verify(webbrowserDelegate);
313313 compare(webbrowserDelegate.stage, ApplicationInfoInterface.MainStage);
@@ -317,6 +317,8 @@ Rectangle {
317317 tryCompare(webbrowserWindow.surface, "activeFocus", true);
318318
319319 var dialerSurfaceId = launchApp("dialer");
320+ dialerCheckBox.checked = true;
321+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
320322
321323 var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
322324 var dialerDelegate = findChild(stage, "appDelegate_" + dialerSurfaceId);
@@ -347,6 +349,8 @@ Rectangle {
347349 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
348350
349351 var dialerSurfaceId = launchApp("dialer");
352+ dialerCheckBox.checked = true;
353+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
350354
351355 performEdgeSwipeToShowAppSpread();
352356
@@ -356,7 +360,7 @@ Rectangle {
356360 compare(appDelegate.stage, ApplicationInfoInterface.SideStage);
357361 tryCompare(dragArea, "closeable", true);
358362
359− swipeSurfaceUpwards(dialerSurfaceId);
363+ swipeSurfaceDownwards(dialerSurfaceId);
360364
361365 // Check that dialer-app has been closed
362366
@@ -367,10 +371,16 @@ Rectangle {
367371 tryCompareFunction(function() {
368372 return ApplicationManager.findApplication(dialerCheckBox.appId);
369373 }, null);
374+
375+ stage.closeSpread();
370376 }
371377
372378 function test_suspendsAndResumesAppsInMainStage() {
373379 var webbrowserSurfaceId = launchApp("morph");
380+
381+ webbrowserCheckBox.checked = true;
382+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
383+
374384 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
375385 var webbrowserDelegate = findChild(stage, "appDelegate_" + webbrowserSurfaceId);
376386 compare(webbrowserDelegate.stage, ApplicationInfoInterface.MainStage);
@@ -378,6 +388,9 @@ Rectangle {
378388 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
379389
380390 var gallerySurfaceId = launchApp("gallery");
391+ galleryCheckBox.checked = true;
392+ waitUntilAppSurfaceShowsUp(gallerySurfaceId);
393+
381394 var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
382395 var galleryDelegate = findChild(stage, "appDelegate_" + gallerySurfaceId);
383396 compare(galleryDelegate.stage, ApplicationInfoInterface.MainStage);
@@ -406,13 +419,20 @@ Rectangle {
406419 // launch two main stage apps
407420 // gallery will be on foreground and webbrowser on background
408421
422+
409423 var webbrowserSurfaceId = launchApp("morph");
424+
425+ webbrowserCheckBox.checked = true;
426+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
427+
410428 var webbrowserDelegate = findChild(stage, "appDelegate_" + webbrowserSurfaceId);
411429 verify(webbrowserDelegate);
412430 compare(webbrowserDelegate.stage, ApplicationInfoInterface.MainStage);
413431 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
414432
415433 var gallerySurfaceId = launchApp("gallery");
434+ galleryCheckBox.checked = true;
435+ waitUntilAppSurfaceShowsUp(gallerySurfaceId);
416436 var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
417437 var galleryDelegate = findChild(stage, "appDelegate_" + gallerySurfaceId);
418438 compare(galleryDelegate.stage, ApplicationInfoInterface.MainStage);
@@ -423,12 +443,16 @@ Rectangle {
423443 // facebook will be on foreground and dialer on background
424444
425445 var dialerSurfaceId = launchApp("dialer");
446+ dialerCheckBox.checked = true;
447+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
426448 var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
427449 var dialerDelegate = findChild(stage, "appDelegate_" + dialerSurfaceId);
428450 compare(dialerDelegate.stage, ApplicationInfoInterface.SideStage);
429451
430452 var facebookSurfaceId = launchApp("facebook");
431− var facebookApp = ApplicationManager.findApplication(facebookCheckBox.appId);
453+ facebookCheckBox.checked = true;
454+ waitUntilAppSurfaceShowsUp(facebookSurfaceId);
455+
432456 var facebookDelegate = findChild(stage, "appDelegate_" + facebookSurfaceId);
433457 compare(facebookDelegate.stage, ApplicationInfoInterface.SideStage);
434458
@@ -461,12 +485,16 @@ Rectangle {
461485 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
462486
463487 var webbrowserSurfaceId = launchApp("morph");
488+ webbrowserCheckBox.checked = true;
489+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
464490 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
465491 var webbrowserDelegate = findChild(stage, "appDelegate_" + webbrowserSurfaceId);
466492 compare(webbrowserDelegate.stage, ApplicationInfoInterface.MainStage);
467493
468494 var dialerSurfaceId = launchApp("dialer");
469− var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
495+ dialerCheckBox.checked = true;
496+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
497+
470498 var dialerDelegate = findChild(stage, "appDelegate_" + dialerSurfaceId);
471499 compare(dialerDelegate.stage, ApplicationInfoInterface.SideStage);
472500
@@ -539,7 +567,7 @@ Rectangle {
539567 tryCompare(stagesPriv, "mainStageAppId", "facebook-webapp");
540568 tryCompare(stagesPriv, "sideStageAppId", "");
541569
542− var appSurfaceId = topSurfaceList.nextId;
570+ var appSurfaceId = topLevelSurfaceList.nextId;
543571 var app = ApplicationManager.startApplication(data.appId);
544572 waitUntilAppSurfaceShowsUp(appSurfaceId);
545573
@@ -566,6 +594,8 @@ Rectangle {
566594 tryCompare(stagesPriv, "sideStageAppId", "");
567595
568596 var webbrowserSurfaceId = launchApp("morph");
597+ webbrowserCheckBox.checked = true;
598+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
569599
570600 tryCompare(stagesPriv, "mainStageAppId", data.mainStageAppId);
571601 tryCompare(stagesPriv, "sideStageAppId", data.sideStageAppId);
@@ -591,6 +621,8 @@ Rectangle {
591621 tryCompare(stagesPriv, "sideStageAppId", "");
592622
593623 var webbrowserSurfaceId = launchApp("morph");
624+ webbrowserCheckBox.checked = true;
625+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
594626
595627 if (data.toStage === ApplicationInfoInterface.SideStage) {
596628 dragToSideStage(webbrowserSurfaceId);
@@ -606,6 +638,8 @@ Rectangle {
606638 function test_loadSideStageByDraggingFromMainStage() {
607639 sideStage.showNow();
608640 var webbrowserSurfaceId = launchApp("morph")
641+ webbrowserCheckBox.checked = true;
642+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
609643
610644 var appDelegate = findChild(stage, "appDelegate_" + webbrowserSurfaceId);
611645 verify(appDelegate);
@@ -625,6 +659,8 @@ Rectangle {
625659 return WindowStateStorage.getStage(webbrowserCheckBox.appId, ApplicationInfoInterface.MainStage) === ApplicationInfoInterface.SideStage
626660 }, true);
627661 var webbrowserSurfaceId = launchApp("morph");
662+ webbrowserCheckBox.checked = true;
663+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
628664
629665 var appDelegate = findChild(stage, "appDelegate_" + webbrowserSurfaceId);
630666 verify(appDelegate);
@@ -638,7 +674,7 @@ Rectangle {
638674
639675 /*
640676 1- Suspended app gets killed behind the scenes, causing its surface to go zombie.
641− 2- Surface gets screenshotted and removed. Its slot in the topSurfaceList remains,
677+ 2- Surface gets screenshotted and removed. Its slot in the topLevelSurfaceList remains,
642678 though (so ApplicationWindow can display the screenshot in its place).
643679 3- User taps on the screenshot of the long-gone surface.
644680
@@ -653,6 +689,9 @@ Rectangle {
653689
654690 var webbrowserSurfaceId = topSurfaceList.nextId;
655691 launchApp("morph");
692+
693+ webbrowserCheckBox.checked = true;
694+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
656695 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
657696
658697 switchToSurface(facebookSurfaceId);
@@ -665,24 +704,22 @@ Rectangle {
665704 // simulate the suspended app being killed by the out-of-memory daemon
666705 webbrowserApp.surfaceList.get(0).setLive(false);
667706
668− // wait until the surface is gone
669− tryCompare(webbrowserApp.surfaceList, "count", 0);
670− compare(topSurfaceList.surfaceAt(topSurfaceList.indexForId(webbrowserSurfaceId)), null);
671−
672707 switchToSurface(webbrowserSurfaceId);
673708
674709 // webbrowser should have been brought to front
675− tryCompareFunction(function(){return topSurfaceList.idAt(0);}, webbrowserSurfaceId);
710+ tryCompareFunction(function(){return topLevelSurfaceList.idAt(0);}, webbrowserSurfaceId);
676711
677712 // and it should eventually get a new surface and get resumed
678− tryCompareFunction(function(){return topSurfaceList.surfaceAt(0) !== null;}, true);
679− compare(topSurfaceList.count, 2); // still two top-level items
713+ tryCompareFunction(function(){return topLevelSurfaceList.surfaceAt(0) !== null;}, true);
714+ compare(topLevelSurfaceList.count, 2); // still two top-level items
680715 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
681716 compare(webbrowserApp.surfaceList.count, 1);
682717 }
683718
684719 function test_draggingSurfaceKeepsSurfaceFocus() {
685720 var webbrowserSurfaceId = launchApp("morph");
721+ webbrowserCheckBox.checked = true;
722+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
686723
687724 var appDelegate = findChild(stage, "appDelegate_" + webbrowserSurfaceId);
688725 verify(appDelegate);
@@ -699,6 +736,8 @@ Rectangle {
699736 function test_switchRestoreStageOnRotation() {
700737 WindowStateStorage.saveStage(webbrowserCheckBox.appId, ApplicationInfoInterface.SideStage)
701738 var webbrowserSurfaceId = launchApp("morph");
739+ webbrowserCheckBox.checked = true;
740+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
702741
703742 var appDelegate = findChild(stage, "appDelegate_" + webbrowserSurfaceId);
704743 verify(appDelegate);
@@ -716,6 +755,8 @@ Rectangle {
716755
717756 function test_restoreSavedStageOnCloseReopen() {
718757 var webbrowserSurfaceId = launchApp("morph");
758+ webbrowserCheckBox.checked = true;
759+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
719760
720761 var appDelegate = findChild(stage, "appDelegate_" + webbrowserSurfaceId);
721762 verify(appDelegate);
@@ -732,6 +773,8 @@ Rectangle {
732773 stage.shellOrientation = Qt.LandscapeOrientation;
733774
734775 webbrowserSurfaceId = launchApp("morph");
776+ webbrowserCheckBox.checked = true;
777+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
735778
736779 appDelegate = findChild(stage, "appDelegate_" + webbrowserSurfaceId);
737780 verify(appDelegate);
tests/qmltests/Stage/tst_WindowDecoration.qml−2
@@ -47,10 +47,8 @@ Item {
4747 property string name: "webbrowser"
4848 }
4949
50− SurfaceManager { id: sMgr }
5150 ApplicationMenuDataLoader {
5251 id: appMenuData
53− surfaceManager: sMgr
5452 }
5553
5654 UnityMenuModel {
tests/qmltests/Stage/tst_WindowResizeArea.qml+1−8
@@ -19,7 +19,6 @@ import QtQuick.Layouts 1.1
1919import QtTest 1.0
2020import Unity.Test 0.1
2121import ".."
22−import "../../../qml/Components/PanelState"
2322import "../../../qml/Stage"
2423import Ubuntu.Components 1.3
2524import Ubuntu.Components.ListItems 1.3 as ListItem
@@ -31,12 +30,6 @@ Item {
3130 height: units.gu(60)
3231 width: units.gu(85)
3332
34− Binding {
35− target: PanelState
36− property: "panelHeight"
37− value: units.gu(3)
38− }
39−
4033 Component {
4134 id: fakeWindowComponent
4235
@@ -362,7 +355,7 @@ Item {
362355
363356 // Make sure it's again where we left it in normal state before destroying
364357 compare(fakeWindow.requestedX >= 0, true)
365− compare(fakeWindow.requestedY >= PanelState.panelHeight, true)
358+ compare(fakeWindow.requestedY >= 0, true)
366359 compare(fakeWindow.requestedX + fakeWindow.width <= root.width, true)
367360 compare(fakeWindow.requestedY + fakeWindow.height <= root.height, true)
368361
tests/qmltests/Tutorial/tst_Tutorial.qml+5−23
@@ -37,20 +37,10 @@ Rectangle {
3737 height: units.gu(71)
3838
3939 QtObject {
40− id: applicationArguments
41−
42− function hasGeometry() {
43− return false;
44− }
45−
46− function width() {
47− return 0;
48− }
49−
50− function height() {
51− return 0;
52− }
40+ id: _screenWindow
41+ property bool primary: true
5342 }
43+ property alias screenWindow: _screenWindow
5444
5545 Telephony.CallEntry {
5646 id: phoneCall
@@ -256,13 +246,7 @@ Rectangle {
256246 property real halfWidth: shell ? shell.width / 2 : 0
257247 property real halfHeight: shell ? shell.height / 2 : 0
258248
259− onShellChanged: {
260− if (shell) {
261− topLevelSurfaceList = testCase.findInvisibleChild(shell, "topLevelSurfaceList");
262− } else {
263− topLevelSurfaceList = null;
264− }
265− }
249+ topLevelSurfaceList: shell ? shell.topLevelSurfaceList : null;
266250
267251 function init() {
268252 prepareShell();
@@ -327,9 +311,7 @@ Rectangle {
327311 }
328312
329313 function ensureInputMethodSurface() {
330− var surfaceManager = findInvisibleChild(shell, "surfaceManager");
331− verify(surfaceManager);
332− surfaceManager.createInputMethodSurface();
314+ SurfaceManager.createInputMethodSurface();
333315
334316 tryCompareFunction(function() { return topLevelSurfaceList.inputMethodSurface !== null }, true);
335317 }
tests/qmltests/tst_OrientedShell.qml+22−10
@@ -30,7 +30,6 @@ import Utils 0.1
3030
3131import "../../qml"
3232import "../../qml/Components"
33−import "../../qml/Components/PanelState"
3433import "Stage"
3534
3635Rectangle {
@@ -82,6 +81,12 @@ Rectangle {
8281 deviceFilter: InputInfo.Keyboard
8382 }
8483
84+ QtObject {
85+ id: _screenWindow
86+ property bool primary: true
87+ }
88+ property alias screenWindow: _screenWindow
89+
8590 property int physicalOrientation0
8691 property int physicalOrientation90
8792 property int physicalOrientation180
@@ -444,8 +449,17 @@ Rectangle {
444449 when: windowShown
445450
446451 property Item orientedShell: orientedShellLoader.status === Loader.Ready ? orientedShellLoader.item : null
447− property Item shell
448− property QtObject topLevelSurfaceList
452+ property Item shell: null
453+ property QtObject topLevelSurfaceList : shell ? shell.topLevelSurfaceList : null
454+ property var panelState: undefined
455+
456+ onOrientedShellChanged: {
457+ if (orientedShell) {
458+ panelState = findInvisibleChild(orientedShell, "panelState");
459+ } else {
460+ panelState = undefined;
461+ }
462+ }
449463
450464 SignalSpy { id: signalSpy }
451465 SignalSpy { id: signalSpy2 }
@@ -504,7 +518,6 @@ Rectangle {
504518 function cleanup() {
505519 tryCompare(shell, "waitingOnGreeter", false, 10000); // make sure greeter didn't leave us in disabled state
506520 shell = null;
507− topLevelSurfaceList = null;
508521
509522 tearDown();
510523 }
@@ -1462,8 +1475,7 @@ Rectangle {
14621475 removeTimeConstraintsFromSwipeAreas(orientedShellLoader.item);
14631476
14641477 shell = findChild(orientedShell, "shell");
1465−
1466− topLevelSurfaceList = findInvisibleChild(shell, "topLevelSurfaceList");
1478+ verify(shell);
14671479 verify(topLevelSurfaceList);
14681480
14691481 tryCompare(shell, "waitingOnGreeter", false); // reset by greeter when ready
@@ -1510,13 +1522,13 @@ Rectangle {
15101522
15111523 switch (expectedAngle) {
15121524 case 0:
1513− return point.x === 0 && point.y === PanelState.panelHeight;
1525+ return point.x === 0 && point.y === panelState.panelHeight;
15141526 case 90:
1515− return point.x === orientedShell.width - PanelState.panelHeight && point.y === 0;
1527+ return point.x === orientedShell.width - panelState.panelHeight && point.y === 0;
15161528 case 180:
1517− return point.x === orientedShell.width && point.y === orientedShell.height - PanelState.panelHeight;
1529+ return point.x === orientedShell.width && point.y === orientedShell.height - panelState.panelHeight;
15181530 default: // 270
1519− return point.x === PanelState.panelHeight && point.y === orientedShell.height;
1531+ return point.x === panelState.panelHeight && point.y === orientedShell.height;
15201532 }
15211533 }
15221534
tests/qmltests/tst_Shell.qml+40−28
@@ -38,7 +38,6 @@ import Unity.Indicators 0.1 as Indicators
3838
3939import "../../qml"
4040import "../../qml/Components"
41−import "../../qml/Components/PanelState"
4241import "Stage"
4342
4443Rectangle {
@@ -57,18 +56,22 @@ Rectangle {
5756 id: appMenuData
5857 }
5958
60− property var shell: shellLoader.item ? shellLoader.item : null
59+ readonly property var shell: shellLoader.item ? shellLoader.item : null
6160 onShellChanged: {
6261 if (shell) {
63− topLevelSurfaceList = testCase.findInvisibleChild(shell, "topLevelSurfaceList");
64− appMenuData.surfaceManager = testCase.findInvisibleChild(shell, "surfaceManager");
62+ panelState = testCase.findInvisibleChild(shell, "panelState");
6563 } else {
66− topLevelSurfaceList = null;
67− appMenuData.surfaceManager = null;
64+ panelState = null;
6865 }
6966 }
67+ property var panelState: null
68+ readonly property alias topLevelSurfaceList: testCase.topLevelSurfaceList
7069
71− property var topLevelSurfaceList: null
70+ QtObject {
71+ id: _screenWindow
72+ property bool primary: true
73+ }
74+ property alias screenWindow: _screenWindow
7275
7376 Item {
7477 id: shellContainer
@@ -259,6 +262,7 @@ Rectangle {
259262 anchors { left: parent.left; right: parent.right }
260263 activeFocusOnPress: false
261264 model: ["phone", "tablet", "desktop"]
265+ selectedIndex: 2
262266 onSelectedIndexChanged: {
263267 shellLoader.state = model[selectedIndex];
264268 }
@@ -272,6 +276,7 @@ Rectangle {
272276 anchors { left: parent.left; right: parent.right }
273277 activeFocusOnPress: false
274278 model: ["phone", "tablet", "desktop"]
279+ selectedIndex: 0
275280 }
276281 MouseTouchEmulationCheckbox {
277282 id: mouseEmulation
@@ -557,6 +562,7 @@ Rectangle {
557562 when: windowShown
558563
559564 property Item shell: shellLoader.status === Loader.Ready ? shellLoader.item : null
565+ topLevelSurfaceList: shell ? shell.topLevelSurfaceList : null
560566
561567 function initTestCase() {
562568 tearDown();
@@ -595,7 +601,6 @@ Rectangle {
595601 waitForGreeterToStabilize();
596602
597603 // from StageTestCase
598− topLevelSurfaceList = findInvisibleChild(shell, "topLevelSurfaceList");
599604 verify(topLevelSurfaceList);
600605 stage = findChild(shell, "stage");
601606 }
@@ -672,9 +677,7 @@ Rectangle {
672677 }
673678
674679 function ensureInputMethodSurface() {
675− var surfaceManager = findInvisibleChild(shell, "surfaceManager");
676− verify(surfaceManager);
677− surfaceManager.createInputMethodSurface();
680+ SurfaceManager.createInputMethodSurface();
678681
679682 tryCompareFunction(function() { return root.topLevelSurfaceList.inputMethodSurface !== null }, true);
680683 }
@@ -1735,9 +1738,9 @@ Rectangle {
17351738 var spreadDelegate2 = appRepeater.itemAt(2);
17361739 var closeMouseArea = findChild(spreadDelegate2, "closeMouseArea");
17371740
1738− // Move the mosue over tile 2 and verify the close button becomes visible
1741+ // Move the mouse over tile 2 and verify the close button becomes visible
17391742 var x = 0;
1740− var y = shell.height * .5;
1743+ var y = shell.height * .6;
17411744 mouseMove(shell, x, y)
17421745 while (spreadItem.highlightedIndex !== 2 && x <= 4000) {
17431746 x+=10;
@@ -1745,6 +1748,7 @@ Rectangle {
17451748 wait(10); // spin the loop so bindings get evaluated
17461749 }
17471750 tryCompare(closeMouseArea, "enabled", true)
1751+ waitForRendering(shell)
17481752
17491753 var countBeforeClickingCloseButton = topLevelSurfaceList.count;
17501754 verify(topLevelSurfaceList.indexForId(surfaceId) === 2);
@@ -1789,7 +1793,7 @@ Rectangle {
17891793
17901794 // Move the mouse over tile 2 and verify the highlight becomes visible
17911795 var x = 0;
1792− var y = shell.height * (data.tileInfo ? .9 : 0.5)
1796+ var y = shell.height * (data.tileInfo ? .9 : 0.7)
17931797 mouseMove(shell, x, y)
17941798 while (spreadItem.highlightedIndex !== 2 && x <= 4000) {
17951799 x+=10;
@@ -1811,6 +1815,15 @@ Rectangle {
18111815 function test_progressiveAutoScrolling() {
18121816 loadDesktopShellWithApps()
18131817
1818+ // load some more apps
1819+ ApplicationManager.startApplication("twitter-webapp")
1820+ ApplicationManager.startApplication("ubuntu-weather-app")
1821+ ApplicationManager.startApplication("notes-app")
1822+ for (var i = 0; i < topLevelSurfaceList.count; ++i) {
1823+ waitUntilAppWindowIsFullyLoaded(topLevelSurfaceList.idAt(i));
1824+ }
1825+
1826+
18141827 var appRepeater = findInvisibleChild(shell, "appRepeater");
18151828 var launcherPanel = findChild(shell, "launcherPanel");
18161829 verify(appRepeater !== null);
@@ -1908,21 +1921,21 @@ Rectangle {
19081921 var maximizeButton = findChild(appDelegate, "maximizeWindowButton");
19091922
19101923 tryCompare(appDelegate, "state", "normal");
1911− tryCompare(PanelState, "decorationsVisible", false)
1924+ tryCompare(panelState, "decorationsVisible", false)
19121925
19131926 mouseClick(maximizeButton, maximizeButton.width / 2, maximizeButton.height / 2);
19141927 tryCompare(appDelegate, "state", "maximized");
1915− tryCompare(PanelState, "decorationsVisible", true)
1928+ tryCompare(panelState, "decorationsVisible", true)
19161929
19171930 ApplicationManager.stopApplication(application.appId);
1918− tryCompare(PanelState, "decorationsVisible", false)
1931+ tryCompare(panelState, "decorationsVisible", false)
19191932
19201933 // wait until all zombie surfaces are gone. As MirSurfaceItems hold references over them.
19211934 // They won't be gone until those surface items are destroyed.
19221935 tryCompareFunction(function() { return application.surfaceList.count }, 0);
19231936
19241937 ApplicationManager.startApplication(application.appId);
1925− tryCompare(PanelState, "decorationsVisible", true)
1938+ tryCompare(panelState, "decorationsVisible", true)
19261939 }
19271940
19281941 function test_newAppHasValidGeometry() {
@@ -1979,7 +1992,7 @@ Rectangle {
19791992 mousePress(appDelegate, appDelegate.width / 2, units.gu(1))
19801993 mouseMove(appDelegate, appDelegate.width / 2, -units.gu(100))
19811994
1982− tryVerify(function() {return (appDelegate.y >= PanelState.panelHeight)});
1995+ tryVerify(function() {return (appDelegate.y >= panelState.panelHeight)});
19831996 }
19841997
19851998 function test_cantResizeWindowUnderPanel() {
@@ -2007,7 +2020,7 @@ Rectangle {
20072020 mouseMove(decoration, decoration.width/2, -units.gu(100));
20082021
20092022 // verify we don't go past the panel
2010− tryVerify(function() {return (appDelegate.y >= PanelState.panelHeight)});
2023+ tryVerify(function() {return (appDelegate.y >= panelState.panelHeight)});
20112024 }
20122025
20132026 function test_restoreWindowStateFixesIfUnderPanel() {
@@ -2031,7 +2044,7 @@ Rectangle {
20312044 tryCompareFunction(function () { return topLevelSurfaceList.applicationAt(0).appId; }, application.appId);
20322045
20332046 appDelegate = appRepeater.itemAt(0);
2034− tryVerify(function() {return (appDelegate.y >= PanelState.panelHeight)});
2047+ tryVerify(function() {return (appDelegate.y >= panelState.panelHeight)});
20352048 }
20362049
20372050 function test_lifecyclePolicyForNonTouchApp_data() {
@@ -2698,9 +2711,9 @@ Rectangle {
26982711
26992712 function test_oskDisplacesWindow_data() {
27002713 return [
2701− {tag: "no need to displace", windowHeight: units.gu(10), windowY: units.gu(5), targetDisplacement: units.gu(5), oskEnabled: true},
2702− {tag: "displace to top", windowHeight: units.gu(50), windowY: units.gu(10), targetDisplacement: PanelState.panelHeight, oskEnabled: true},
2703− {tag: "osk not on this screen", windowHeight: units.gu(40), windowY: units.gu(10), targetDisplacement: units.gu(10), oskEnabled: false},
2714+ {tag: "no need to displace", windowHeight: units.gu(10), windowY: units.gu(5), targetDisplacement: function() { return units.gu(5); }, oskEnabled: true},
2715+ {tag: "displace to top", windowHeight: units.gu(50), windowY: units.gu(10), targetDisplacement: function() { return panelState.panelHeight; }, oskEnabled: true},
2716+ {tag: "osk not on this screen", windowHeight: units.gu(40), windowY: units.gu(10), targetDisplacement: function() { return units.gu(10); }, oskEnabled: false},
27042717 ]
27052718 }
27062719
@@ -2726,11 +2739,10 @@ Rectangle {
27262739 dashAppDelegate.windowedY = data.windowY;
27272740 topLevelSurfaceList.inputMethodSurface.setInputBounds(Qt.rect(0, 0, 0, 0));
27282741 var initialY = dashAppDelegate.y;
2729− print("intial", initialY, "panel", PanelState.panelHeight);
2730− verify(initialY > PanelState.panelHeight);
2742+ verify(initialY > panelState.panelHeight);
27312743
27322744 topLevelSurfaceList.inputMethodSurface.setInputBounds(Qt.rect(0, root.height / 2, root.width, root.height / 2));
2733− tryCompare(dashAppDelegate, "y", data.targetDisplacement);
2745+ tryCompare(dashAppDelegate, "y", data.targetDisplacement());
27342746
27352747 topLevelSurfaceList.inputMethodSurface.setInputBounds(Qt.rect(0, 0, 0, 0));
27362748 tryCompare(dashAppDelegate, "y", initialY);
@@ -3161,7 +3173,7 @@ Rectangle {
31613173 // double click the panel
31623174 var panel = findChild(shell, "panel");
31633175 verify(panel);
3164− mouseDoubleClickSequence(panel, panel.width/2, PanelState.panelHeight/2, Qt.LeftButton, Qt.NoModifier, 300);
3176+ mouseDoubleClickSequence(panel, panel.width/2, panelState.panelHeight/2, Qt.LeftButton, Qt.NoModifier, 300);
31653177 tryCompare(appDelegate, "state", "restored");
31663178 }
31673179
tests/qmltests/tst_ShellApplication.qmlnew file+218
@@ -0,0 +1,218 @@
1+/*
2+ * Copyright (C) 2017 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 LightDM.FullLightDM 0.1 as LightDM
20+import LightDMController 0.1
21+import Unity.Application 0.1
22+import Unity.Test 0.1
23+import Unity.InputInfo 0.1
24+
25+import "../../qml"
26+import "../../qml/Components"
27+import "Stage"
28+
29+Rectangle {
30+ id: root
31+ color: "grey"
32+ width: units.gu(100)
33+ height: units.gu(71)
34+
35+ QtObject {
36+ id: args
37+ property bool hasFullscreen: false
38+ property bool hasFrameless: false
39+ property bool hasGeometry: true
40+ property size windowGeometry: Qt.size(root.width, root.height)
41+ property string deviceName: "desktop"
42+ property string mode: "full-greeter"
43+ }
44+ property alias applicationArguments: args
45+
46+ Component.onCompleted: {
47+ // must set the mock mode before loading the Shell
48+ LightDMController.userMode = "single";
49+ }
50+
51+ Flickable {
52+ id: controls
53+ contentHeight: controlRect.height
54+
55+ anchors.top: root.top
56+ anchors.bottom: root.bottom
57+ anchors.right: root.right
58+ width: units.gu(30)
59+
60+ Rectangle {
61+ id: controlRect
62+ anchors { left: parent.left; right: parent.right }
63+ color: "darkgrey"
64+ height: childrenRect.height + units.gu(2)
65+
66+ Column {
67+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
68+ spacing: units.gu(1)
69+
70+ Flow {
71+ spacing: units.gu(1)
72+ anchors { left: parent.left; right: parent.right }
73+
74+ Button {
75+ text: "Show Greeter"
76+ activeFocusOnPress: false
77+ onClicked: {
78+ LightDM.Greeter.showGreeter();
79+ }
80+ }
81+ Button {
82+ text: "Hide Greeter"
83+ activeFocusOnPress: false
84+ onClicked: {
85+ LightDM.Greeter.hideGreeter()
86+ }
87+ }
88+ }
89+ Button {
90+ anchors {
91+ left: parent.left
92+ right: parent.right
93+ }
94+ action: addMouseAction
95+ color: addMouseAction.checked ? UbuntuColors.red : UbuntuColors.green
96+ }
97+
98+ Button {
99+ anchors {
100+ left: parent.left
101+ right: parent.right
102+ }
103+ action: addKBAction
104+ color: addKBAction.checked ? UbuntuColors.red : UbuntuColors.green
105+ }
106+
107+ MouseTouchEmulationCheckbox {
108+ id: mouseEmulation
109+ checked: true
110+ }
111+
112+ Row {
113+ anchors { left: parent.left; right: parent.right }
114+ CheckBox {
115+ id: autohideLauncherCheckbox
116+ onCheckedChanged: {
117+ GSettingsController.setAutohideLauncher(checked)
118+ }
119+ }
120+ Label {
121+ text: "Autohide launcher"
122+ }
123+ }
124+
125+ Label { text: "Applications"; font.bold: true }
126+
127+ Button {
128+ text: "Start all apps"
129+ width: parent.width
130+ activeFocusOnPress: false
131+ onClicked: {
132+ for (var i = 0; i < ApplicationManager.availableApplications.length; i++) {
133+ var appId = ApplicationManager.availableApplications[i];
134+ ApplicationManager.startApplication(appId)
135+ }
136+ }
137+ }
138+
139+ Repeater {
140+ id: appCheckBoxRepeater
141+ model: ApplicationManager.availableApplications
142+ ApplicationCheckBox {
143+ appId: modelData
144+ }
145+ }
146+
147+ Label {
148+ text: "Fingerprint"
149+ }
150+ Row {
151+ Button {
152+ text: "Success"
153+ onClicked: {
154+ var biometryd = testCase.findInvisibleChild(shellContainer, "biometryd");
155+ var uid = 0;
156+ for (var i = 0; i < LightDM.Users.count; i++) {
157+ if (LightDM.Users.data(i, LightDM.UserRoles.NameRole) == AccountsService.user) {
158+ uid = LightDM.Users.data(i, LightDM.UserRoles.UidRole);
159+ break;
160+ }
161+ }
162+ biometryd.operation.mockSuccess(uid);
163+ }
164+ }
165+
166+ Button {
167+ text: "Failure"
168+ onClicked: {
169+ var biometryd = testCase.findInvisibleChild(shellContainer, "biometryd");
170+ biometryd.operation.mockFailure("error");
171+ }
172+ }
173+ }
174+ }
175+ }
176+ }
177+
178+ Action {
179+ id: addMouseAction
180+ text: checked ? "Remove Mouse" : "Add Mouse"
181+ onTriggered: {
182+ if (checked) {
183+ console.log("ADD")
184+ MockInputDeviceBackend.addMockDevice("/mouse0", InputInfo.Mouse);
185+ } else {
186+ console.log("REMOVE")
187+ MockInputDeviceBackend.removeDevice("/mouse0");
188+ }
189+ }
190+ iconName: "input-mouse-symbolic"
191+ checkable: true
192+ checked: false
193+ }
194+
195+ Action {
196+ id: addKBAction
197+ text: checked ? "Remove Keyboard" : "Add Keyboard"
198+ onTriggered: {
199+ if (checked) {
200+ MockInputDeviceBackend.addMockDevice("/kbd0", InputInfo.Keyboard);
201+ } else {
202+ MockInputDeviceBackend.removeDevice("/kbd0");
203+ }
204+ }
205+ iconName: "input-keyboard-symbolic"
206+ checkable: true
207+ checked: false
208+ }
209+
210+ ShellApplication {
211+ }
212+
213+ UnityTestCase {
214+ id: testCase
215+ name: "ShellApplication"
216+ when: windowShown
217+ }
218+}
tests/qmltests/tst_ShellWithPin.qml+4−16
@@ -40,20 +40,10 @@ Item {
4040 }
4141
4242 QtObject {
43− id: applicationArguments
44−
45− function hasGeometry() {
46− return false;
47− }
48−
49− function width() {
50− return 0;
51− }
52−
53− function height() {
54− return 0;
55− }
43+ id: _screenWindow
44+ property bool primary: true
5645 }
46+ property alias screenWindow: _screenWindow
5747
5848 Row {
5949 id: contentRow
@@ -123,6 +113,7 @@ Item {
123113 when: windowShown
124114
125115 property Item shell: shellLoader.status === Loader.Ready ? shellLoader.item : null
116+ topLevelSurfaceList: shell ? shell.topLevelSurfaceList : null
126117
127118 function init() {
128119 tryCompare(shell, "waitingOnGreeter", false); // will be set when greeter is all ready
@@ -138,7 +129,6 @@ Item {
138129
139130 // from StageTestCase
140131 stage = findChild(shell, "stage");
141− topLevelSurfaceList = findInvisibleChild(shell, "topLevelSurfaceList");
142132 verify(topLevelSurfaceList);
143133
144134 startApplication("unity8-dash");
@@ -147,8 +137,6 @@ Item {
147137 function cleanup() {
148138 tryCompare(shell, "waitingOnGreeter", false); // make sure greeter didn't leave us in disabled state
149139
150− topLevelSurfaceList = null;
151−
152140 shellLoader.itemDestroyed = false
153141
154142 shellLoader.active = false
tests/uqmlscene/main.cpp+2
@@ -479,6 +479,8 @@ int main(int argc, char ** argv)
479479 // TODO: as soon as the engine construction completes, the debug service is
480480 // listening for connections. But actually we aren't ready to debug anything.
481481 QQmlEngine engine;
482+ engine.rootContext()->setContextProperty("DebuggingController", new DebuggingController(&app));
483+
482484 QQmlComponent *component = new QQmlComponent(&engine);
483485 for (int i = 0; i < imports.size(); ++i)
484486 engine.addImportPath(imports.at(i));
tests/utils/modules/Unity/Test/StageTestCase.qml+1−1
@@ -62,7 +62,7 @@ UnityTestCase {
6262
6363 return findChild(stage, "appDelegate_" + surfaceId);
6464 } catch(err) {
65− throw new Error("startApplication("+appId+") called from line " + util.callerLine(1) + " failed!");
65+ throw new Error("startApplication("+appId+") called from line " + util.callerLine(1) + " failed! - " + err);
6666 }
6767 }
6868}
tests/utils/modules/Unity/Test/UnityTestCase.qml+14
@@ -17,6 +17,7 @@
1717import QtQuick 2.4
1818import QtTest 1.0
1919import Unity.Application 0.1
20+import WindowManager 1.0
2021import Ubuntu.Components 1.3
2122import Ubuntu.Test 1.0 as UbuntuTest
2223import Unity.Test 0.1 as UT
@@ -47,6 +48,18 @@ TestCase {
4748 }
4849 }
4950
51+ Binding {
52+ target: WindowManagerObjects
53+ property: "surfaceManager"
54+ value: SurfaceManager
55+ }
56+
57+ Binding {
58+ target: WindowManagerObjects
59+ property: "applicationManager"
60+ value: ApplicationManager
61+ }
62+
5063 // Fake implementation to be provided to items under test
5164 property var fakeDateTime: new function() {
5265 this.currentTimeMs = 0
@@ -641,5 +654,6 @@ TestCase {
641654 tryCompare(application, "state", ApplicationInfo.Stopped);
642655 }
643656 compare(ApplicationManager.count, 0);
657+ SurfaceManager.releaseInputMethodSurface();
644658 }
645659}