on xenial help sign in
merge prereq
commit363c5deda6a379f48d9294b421d9d74058d20c3b authorLukáš Tinkl <lukas.tinkl@canonical.com> dateMon 2 Nov 2015, 21:42 (11 years ago) parent26855ad961a4e5
16 files changed+570−84
plugins/Utils/windowstatestorage.cpp+61−22 plugins/Utils/windowstatestorage.h+13−1 qml/Components/WindowControlButtons.qml+3 qml/Panel/Panel.qml+22−3 qml/Shell.qml+2 qml/Stages/DecoratedWindow.qml+3−1 qml/Stages/DesktopSpread.qml+1−1 qml/Stages/DesktopSpreadDelegate.qml+2−2 qml/Stages/DesktopStage.qml+150−18 qml/Stages/WindowDecoration.qml+6−2 qml/Stages/WindowResizeArea.qml+38−7 tests/mocks/Utils/windowstatestorage.cpp+17 tests/mocks/Utils/windowstatestorage.h+12 tests/qmltests/Stages/tst_DesktopStage.qml+132−25 tests/qmltests/Stages/tst_WindowResizeArea.qml+35−2 tests/qmltests/tst_Shell.qml+73
plugins/Utils/windowstatestorage.cpp+61−22
@@ -47,26 +47,38 @@ WindowStateStorage::~WindowStateStorage()
4747 m_db.close();
4848}
4949
50−void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect)
50+void WindowStateStorage::saveState(const QString &windowId, WindowStateStorage::WindowState state)
5151{
52− QMutexLocker mutexLocker(&s_mutex);
52+ const QString queryString = QStringLiteral("INSERT OR REPLACE INTO state (windowId, state) values ('%1', '%2');")
53+ .arg(windowId)
54+ .arg((int)state);
55+
56+ saveValue(queryString);
57+}
58+
59+WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue) const
60+{
61+ const QString queryString = QStringLiteral("SELECT * FROM state WHERE windowId = '%1';")
62+ .arg(windowId);
5363
54− QString queryString = QStringLiteral("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
64+ QSqlQuery query = getValue(queryString);
65+
66+ if (!query.first()) {
67+ return defaultValue;
68+ }
69+ return (WindowState)query.value("state").toInt();
70+}
71+
72+void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect)
73+{
74+ const QString queryString = QStringLiteral("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
5575 .arg(windowId)
5676 .arg(rect.x())
5777 .arg(rect.y())
5878 .arg(rect.width())
5979 .arg(rect.height());
6080
61− QFuture<void> future = QtConcurrent::run(executeAsyncQuery, queryString);
62− m_asyncQueries.append(future);
63−
64− QFutureWatcher<void> *futureWatcher = new QFutureWatcher<void>();
65− futureWatcher->setFuture(future);
66− connect(futureWatcher, &QFutureWatcher<void>::finished,
67− this,
68− [=](){ m_asyncQueries.removeAll(futureWatcher->future());
69− futureWatcher->deleteLater(); });
81+ saveValue(queryString);
7082}
7183
7284void WindowStateStorage::executeAsyncQuery(const QString &queryString)
@@ -82,20 +94,13 @@ void WindowStateStorage::executeAsyncQuery(const QString &queryString)
8294 }
8395}
8496
85−QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue)
97+QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue) const
8698{
87− QMutexLocker l(&s_mutex);
8899 QString queryString = QStringLiteral("SELECT * FROM geometry WHERE windowId = '%1';")
89100 .arg(windowId);
90− QSqlQuery query;
91101
92− bool ok = query.exec(queryString);
93− if (!ok) {
94− qWarning() << "Error retrieving window state for" << windowId
95− << "Driver error:" << query.lastError().driverText()
96− << "Database error:" << query.lastError().databaseText();
97− return defaultValue;
98− }
102+ QSqlQuery query = getValue(queryString);
103+
99104 if (!query.first()) {
100105 return defaultValue;
101106 }
@@ -114,4 +119,38 @@ void WindowStateStorage::initdb()
114119 QSqlQuery query;
115120 query.exec(QStringLiteral("CREATE TABLE geometry(windowId TEXT UNIQUE, x INTEGER, y INTEGER, width INTEGER, height INTEGER);"));
116121 }
122+
123+ if (!m_db.tables().contains("state")) {
124+ QSqlQuery query;
125+ query.exec("CREATE TABLE state(windowId TEXT UNIQUE, state INTEGER);");
126+ }
127+}
128+
129+void WindowStateStorage::saveValue(const QString &queryString)
130+{
131+ QMutexLocker mutexLocker(&s_mutex);
132+
133+ QFuture<void> future = QtConcurrent::run(executeAsyncQuery, queryString);
134+ m_asyncQueries.append(future);
135+
136+ QFutureWatcher<void> *futureWatcher = new QFutureWatcher<void>();
137+ futureWatcher->setFuture(future);
138+ connect(futureWatcher, &QFutureWatcher<void>::finished,
139+ this,
140+ [=](){ m_asyncQueries.removeAll(futureWatcher->future());
141+ futureWatcher->deleteLater(); });
142+}
143+
144+QSqlQuery WindowStateStorage::getValue(const QString &queryString) const
145+{
146+ QMutexLocker l(&s_mutex);
147+ QSqlQuery query;
148+
149+ bool ok = query.exec(queryString);
150+ if (!ok) {
151+ qWarning() << "Error retrieving database query:" << queryString
152+ << "Driver error:" << query.lastError().driverText()
153+ << "Database error:" << query.lastError().databaseText();
154+ }
155+ return query;
117156}
plugins/Utils/windowstatestorage.h+13−1
@@ -22,16 +22,28 @@
2222class WindowStateStorage: public QObject
2323{
2424 Q_OBJECT
25+ Q_ENUMS(WindowState)
2526public:
27+ enum WindowState {
28+ WindowStateNormal,
29+ WindowStateMaximized
30+ };
31+
2632 WindowStateStorage(QObject *parent = 0);
2733 virtual ~WindowStateStorage();
2834
35+ Q_INVOKABLE void saveState(const QString &windowId, WindowState state);
36+ Q_INVOKABLE WindowState getState(const QString &windowId, WindowState defaultValue) const;
37+
2938 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
30− Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
39+ Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue) const;
3140
3241private:
3342 void initdb();
3443
44+ void saveValue(const QString &queryString);
45+ QSqlQuery getValue(const QString &queryString) const;
46+
3547 static void executeAsyncQuery(const QString &queryString);
3648 static QMutex s_mutex;
3749
qml/Components/WindowControlButtons.qml+3
@@ -28,6 +28,7 @@ Row {
2828 signal maximize()
2929
3030 Rectangle {
31+ objectName: "closeWindowButton"
3132 height: parent.height; width: height; radius: height / 2
3233 gradient: Gradient {
3334 GradientStop { color: "#F49073"; position: 0 }
@@ -38,6 +39,7 @@ Row {
3839 MouseArea { anchors.fill: parent; onClicked: root.close() }
3940 }
4041 Rectangle {
42+ objectName: "minimizeWindowButton"
4143 height: parent.height; width: height; radius: height / 2
4244 gradient: Gradient {
4345 GradientStop { color: "#92918C"; position: 0 }
@@ -48,6 +50,7 @@ Row {
4850 MouseArea { anchors.fill: parent; onClicked: root.minimize() }
4951 }
5052 Rectangle {
53+ objectName: "maximizeWindowButton"
5154 height: parent.height; width: height; radius: height / 2
5255 gradient: Gradient {
5356 GradientStop { color: "#92918C"; position: 0 }
qml/Panel/Panel.qml+22−3
@@ -28,6 +28,7 @@ Item {
2828 property alias callHint: __callHint
2929 property bool fullscreenMode: false
3030 property real indicatorAreaShowProgress: 1.0
31+ property bool locked: false
3132
3233 opacity: fullscreenMode && indicators.fullyClosed ? 0.0 : 1.0
3334
@@ -136,7 +137,7 @@ Item {
136137 }
137138
138139 shown: false
139− width: root.width - (PanelState.buttonsVisible ? windowControlButtons.width : 0)
140+ width: root.width - (windowControlButtons.visible ? windowControlButtons.width + titleLabel.width : 0)
140141 minimizedPanelHeight: units.gu(3)
141142 expandedPanelHeight: units.gu(7)
142143 openedHeight: root.height - indicatorOrangeLine.height
@@ -164,18 +165,36 @@ Item {
164165
165166 WindowControlButtons {
166167 id: windowControlButtons
168+ objectName: "panelWindowControlButtons"
167169 anchors {
168170 left: parent.left
169171 top: parent.top
170172 margins: units.gu(0.7)
171173 }
172174 height: indicators.minimizedPanelHeight - anchors.margins * 2
173− visible: PanelState.buttonsVisible
175+ visible: PanelState.buttonsVisible && !root.locked
174176 onClose: PanelState.close()
175177 onMinimize: PanelState.minimize()
176178 onMaximize: PanelState.maximize()
177179 }
178180
181+ Label {
182+ id: titleLabel
183+ objectName: "windowDecorationTitle"
184+ anchors {
185+ left: windowControlButtons.right
186+ top: parent.top
187+ margins: units.gu(0.7)
188+ }
189+ color: "#DFDBD2"
190+ height: windowControlButtons.height
191+ visible: windowControlButtons.visible
192+ verticalAlignment: Text.AlignVCenter
193+ fontSize: "small"
194+ font.bold: true
195+ text: PanelState.title
196+ }
197+
179198 PanelSeparatorLine {
180199 id: indicatorOrangeLine
181200 anchors {
@@ -189,7 +208,7 @@ Item {
189208 id: __callHint
190209 anchors {
191210 top: parent.top
192− left: PanelState.buttonsVisible ? windowControlButtons.right : parent.left
211+ left: windowControlButtons.visible ? windowControlButtons.right : parent.left
193212 }
194213 height: indicators.minimizedPanelHeight
195214 visible: active && indicators.state == "initial"
qml/Shell.qml+2
@@ -57,6 +57,7 @@ Item {
5757 property bool beingResized
5858 property string usageScenario: "phone" // supported values: "phone", "tablet" or "desktop"
5959 property string mode: "full-greeter"
60+ property bool cursorVisible: false
6061 function updateFocusedAppOrientation() {
6162 applicationsDisplayLoader.item.updateFocusedAppOrientation();
6263 }
@@ -536,6 +537,7 @@ Item {
536537
537538 fullscreenMode: (topmostApplicationIsFullscreen && !lightDM.greeter.active && launcher.progress == 0)
538539 || greeter.hasLockedApp
540+ locked: greeter && greeter.active
539541 }
540542
541543 Launcher {
qml/Stages/DecoratedWindow.qml+3−1
@@ -26,6 +26,7 @@ FocusScope {
2626 property alias window: applicationWindow
2727 property alias application: applicationWindow.application
2828 property alias active: decoration.active
29+ property alias title: decoration.title
2930
3031 property bool decorationShown: true
3132 property bool highlightShown: false
@@ -66,7 +67,8 @@ FocusScope {
6667 objectName: application ? "appWindowDecoration_" + application.appId : "appWindowDecoration_null"
6768 anchors { left: parent.left; top: parent.top; right: parent.right }
6869 height: units.gu(3)
69− title: window.title !== "" ? window.title : model.name
70+ width: root.width
71+ title: window.title !== "" ? window.title : ""
7072 onClose: root.close();
7173 onMaximize: root.maximize();
7274 onMinimize: root.minimize();
qml/Stages/DesktopSpread.qml+1−1
@@ -87,7 +87,7 @@ FocusScope {
8787 function focusSelected() {
8888 if (spreadRepeater.highlightedIndex != -1) {
8989 var application = ApplicationManager.get(spreadRepeater.highlightedIndex);
90− ApplicationManager.focusApplication(application.appId);
90+ ApplicationManager.requestFocusApplication(application.appId);
9191 }
9292 }
9393
qml/Stages/DesktopSpreadDelegate.qml+2−2
@@ -29,8 +29,8 @@ Item {
2929 property bool highlightShown: false
3030 property real shadowOpacity: 1
3131
32− property int windowWidth: application.session && application.session.surface ? application.session.surface.size.width : 0
33− property int windowHeight: application.session && application.session.surface ? application.session.surface.size.height : 0
32+ property int windowWidth: application && application.session && application.session.surface ? application.session.surface.size.width : 0
33+ property int windowHeight: application && application.session && application.session.surface ? application.session.surface.size.height : 0
3434
3535 state: "normal"
3636 states: [
qml/Stages/DesktopStage.qml+150−18
@@ -24,6 +24,7 @@ import "../Components"
2424import "../Components/PanelState"
2525import Utils 0.1
2626import Ubuntu.Gestures 0.1
27+import GlobalShortcut 1.0
2728
2829Rectangle {
2930 id: root
@@ -63,21 +64,72 @@ Rectangle {
6364 spread.state = "";
6465 }
6566
66− ApplicationManager.requestFocusApplication(appId)
67+ ApplicationManager.focusApplication(appId);
68+ }
69+
70+ onApplicationRemoved: {
71+ priv.focusNext();
6772 }
6873
6974 onFocusRequested: {
7075 var appIndex = priv.indexOf(appId);
7176 var appDelegate = appRepeater.itemAt(appIndex);
72− appDelegate.minimized = false;
73− ApplicationManager.focusApplication(appId)
77+ appDelegate.restoreFromMinimized();
7478
7579 if (spread.state == "altTab") {
76− spread.cancel()
80+ spread.cancel();
7781 }
7882 }
7983 }
8084
85+ GlobalShortcut {
86+ id: closeWindowShortcut
87+ shortcut: Qt.AltModifier|Qt.Key_F4
88+ onTriggered: ApplicationManager.stopApplication(priv.focusedAppId)
89+ active: priv.focusedAppId !== ""
90+ }
91+
92+ GlobalShortcut {
93+ id: showSpreadShortcut
94+ shortcut: Qt.MetaModifier|Qt.Key_W
95+ onTriggered: spread.state = "altTab"
96+ }
97+
98+ GlobalShortcut {
99+ id: minimizeAllShortcut
100+ shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_D
101+ onTriggered: priv.minimizeAllWindows()
102+ }
103+
104+ GlobalShortcut {
105+ id: maximizeWindowShortcut
106+ shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Up
107+ onTriggered: priv.focusedAppDelegate.maximize()
108+ active: priv.focusedAppDelegate !== null
109+ }
110+
111+ GlobalShortcut {
112+ id: maximizeWindowLeftShortcut
113+ shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Left
114+ onTriggered: priv.focusedAppDelegate.maximizeLeft()
115+ active: priv.focusedAppDelegate !== null
116+ }
117+
118+ GlobalShortcut {
119+ id: maximizeWindowRightShortcut
120+ shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Right
121+ onTriggered: priv.focusedAppDelegate.maximizeRight()
122+ active: priv.focusedAppDelegate !== null
123+ }
124+
125+ GlobalShortcut {
126+ id: minimizeRestoreShortcut
127+ shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Down
128+ onTriggered: priv.focusedAppDelegate.maximized || priv.focusedAppDelegate.maximizedLeft || priv.focusedAppDelegate.maximizedRight
129+ ? priv.focusedAppDelegate.restore() : priv.focusedAppDelegate.minimize(true)
130+ active: priv.focusedAppDelegate !== null
131+ }
132+
81133 QtObject {
82134 id: priv
83135
@@ -95,6 +147,28 @@ Rectangle {
95147 }
96148 return -1;
97149 }
150+
151+ function minimizeAllWindows() {
152+ for (var i = 0; i < appRepeater.count; i++) {
153+ var appDelegate = appRepeater.itemAt(i);
154+ if (appDelegate && !appDelegate.minimized) {
155+ appDelegate.minimize(false); // minimize but don't switch focus
156+ }
157+ }
158+
159+ ApplicationManager.unfocusCurrentApplication(); // no app should have focus at this point
160+ }
161+
162+ function focusNext() {
163+ ApplicationManager.unfocusCurrentApplication();
164+ for (var i = 0; i < appRepeater.count; i++) {
165+ var appDelegate = appRepeater.itemAt(i);
166+ if (appDelegate && !appDelegate.minimized) {
167+ ApplicationManager.focusApplication(appDelegate.appId);
168+ return;
169+ }
170+ }
171+ }
98172 }
99173
100174 Connections {
@@ -102,16 +176,25 @@ Rectangle {
102176 onClose: {
103177 ApplicationManager.stopApplication(ApplicationManager.focusedApplicationId)
104178 }
105− onMinimize: appRepeater.itemAt(0).minimize();
106− onMaximize: appRepeater.itemAt(0).unmaximize();
179+ onMinimize: appRepeater.itemAt(0).minimize(true);
180+ onMaximize: appRepeater.itemAt(0).restore();
107181 }
108182
109183 Binding {
110184 target: PanelState
111185 property: "buttonsVisible"
112− value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.state === "maximized"
186+ value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.maximized
113187 }
114188
189+ Binding {
190+ target: PanelState
191+ property: "title"
192+ value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.title
193+ when: priv.focusedAppDelegate && priv.focusedAppDelegate.maximized
194+ }
195+
196+ Component.onDestruction: PanelState.buttonsVisible = false;
197+
115198 FocusScope {
116199 id: appContainer
117200 objectName: "appContainer"
@@ -132,18 +215,24 @@ Rectangle {
132215
133216 delegate: FocusScope {
134217 id: appDelegate
218+ objectName: "appDelegate_" + appId
135219 z: ApplicationManager.count - index
136220 y: units.gu(3)
137221 width: units.gu(60)
138222 height: units.gu(50)
139− focus: model.appId === priv.focusedAppId
223+ focus: appId === priv.focusedAppId
140224
141225 property bool maximized: false
226+ property bool maximizedLeft: false
227+ property bool maximizedRight: false
142228 property bool minimized: false
229+ readonly property string appId: model.appId
230+ property bool animationsEnabled: true
231+ property alias title: decoratedWindow.title
143232
144233 onFocusChanged: {
145− if (focus && ApplicationManager.focusedApplicationId !== model.appId) {
146− ApplicationManager.focusApplication(model.appId);
234+ if (focus && ApplicationManager.focusedApplicationId !== appId) {
235+ ApplicationManager.focusApplication(appId);
147236 }
148237 }
149238
@@ -158,27 +247,68 @@ Rectangle {
158247 value: ApplicationInfoInterface.RequestedRunning // Always running for now
159248 }
160249
161− function maximize() {
250+ function maximize(animated) {
251+ animationsEnabled = (animated === undefined) || animated;
162252 minimized = false;
163253 maximized = true;
254+ maximizedLeft = false;
255+ maximizedRight = false;
164256 }
165− function minimize() {
257+ function maximizeLeft() {
258+ minimized = false;
166259 maximized = false;
260+ maximizedLeft = true;
261+ maximizedRight = false;
262+ }
263+ function maximizeRight() {
264+ minimized = false;
265+ maximized = false;
266+ maximizedLeft = false;
267+ maximizedRight = true;
268+ }
269+ function minimize(animated) {
270+ animationsEnabled = (animated === undefined) || animated;
167271 minimized = true;
272+ if (!!animated) {
273+ priv.focusNext();
274+ }
168275 }
169− function unmaximize() {
276+ function restore(animated) {
277+ animationsEnabled = (animated === undefined) || animated;
170278 minimized = false;
171279 maximized = false;
280+ maximizedLeft = false;
281+ maximizedRight = false;
282+ }
283+ function restoreFromMinimized(animated) {
284+ animationsEnabled = (animated === undefined) || animated;
285+ minimized = false;
286+ if (maximized)
287+ maximize();
288+ else if (maximizedLeft)
289+ maximizeLeft();
290+ else if (maximizedRight)
291+ maximizeRight();
292+ ApplicationManager.focusApplication(appId);
172293 }
173294
174295 states: [
175296 State {
176297 name: "normal"; when: !appDelegate.maximized && !appDelegate.minimized
298+ && !appDelegate.maximizedLeft && !appDelegate.maximizedRight
177299 },
178300 State {
179− name: "maximized"; when: appDelegate.maximized
301+ name: "maximized"; when: appDelegate.maximized && !appDelegate.minimized
180302 PropertyChanges { target: appDelegate; x: 0; y: 0; width: root.width; height: root.height }
181303 },
304+ State {
305+ name: "maximizedLeft"; when: appDelegate.maximizedLeft && !appDelegate.minimized
306+ PropertyChanges { target: appDelegate; x: 0; y: units.gu(3); width: root.width/2; height: root.height - units.gu(3) }
307+ },
308+ State {
309+ name: "maximizedRight"; when: appDelegate.maximizedRight && !appDelegate.minimized
310+ PropertyChanges { target: appDelegate; x: root.width/2; y: units.gu(3); width: root.width/2; height: root.height - units.gu(3) }
311+ },
182312 State {
183313 name: "minimized"; when: appDelegate.minimized
184314 PropertyChanges { target: appDelegate; x: -appDelegate.width / 2; scale: units.gu(5) / appDelegate.width; opacity: 0 }
@@ -186,8 +316,9 @@ Rectangle {
186316 ]
187317 transitions: [
188318 Transition {
189− from: "maximized,minimized,normal,"
190− to: "maximized,minimized,normal,"
319+ from: "maximized,maximizedLeft,maximizedRight,minimized,normal,"
320+ to: "maximized,maximizedLeft,maximizedRight,minimized,normal,"
321+ enabled: appDelegate.animationsEnabled
191322 PropertyAnimation { target: appDelegate; properties: "x,y,opacity,width,height,scale" }
192323 },
193324 Transition {
@@ -234,8 +365,9 @@ Rectangle {
234365 focus: true
235366
236367 onClose: ApplicationManager.stopApplication(model.appId)
237− onMaximize: appDelegate.maximize()
238− onMinimize: appDelegate.minimize()
368+ onMaximize: appDelegate.maximized || appDelegate.maximizedLeft || appDelegate.maximizedRight
369+ ? appDelegate.restore() : appDelegate.maximize()
370+ onMinimize: appDelegate.minimize(true)
239371 onDecorationPressed: { ApplicationManager.focusApplication(model.appId) }
240372 }
241373 }
qml/Stages/WindowDecoration.qml+6−2
@@ -45,14 +45,15 @@ MouseArea {
4545 priv.distanceX = pos.x;
4646 priv.distanceY = pos.y;
4747 priv.dragging = true;
48− Mir.cursorName = "grabbing";
4948 } else {
5049 priv.dragging = false;
5150 Mir.cursorName = "";
5251 }
5352 }
53+
5454 onPositionChanged: {
5555 if (priv.dragging) {
56+ Mir.cursorName = "grabbing";
5657 var pos = mapToItem(root.target.parent, mouseX, mouseY);
5758 root.target.x = pos.x - priv.distanceX;
5859 root.target.y = pos.y - priv.distanceY;
@@ -70,11 +71,12 @@ MouseArea {
7071 }
7172
7273 Row {
73− anchors { left: parent.left; top: parent.top; bottom: parent.bottom; margins: units.gu(0.7) }
74+ anchors { fill: parent; margins: units.gu(0.7) }
7475 spacing: units.gu(1)
7576 opacity: root.active ? 1 : 0.5
7677
7778 WindowControlButtons {
79+ id: buttons
7880 height: parent.height
7981 onClose: root.close();
8082 onMinimize: root.minimize();
@@ -86,9 +88,11 @@ MouseArea {
8688 objectName: "windowDecorationTitle"
8789 color: "#DFDBD2"
8890 height: parent.height
91+ width: parent.width - buttons.width - parent.anchors.rightMargin - parent.anchors.leftMargin
8992 verticalAlignment: Text.AlignVCenter
9093 fontSize: "small"
9194 font.bold: true
95+ elide: Text.ElideRight
9296 }
9397 }
9498}
qml/Stages/WindowResizeArea.qml+38−7
@@ -36,18 +36,49 @@ MouseArea {
3636 property int minWidth: 0
3737 property int minHeight: 0
3838
39+ QtObject {
40+ id: priv
41+
42+ property int normalX: 0
43+ property int normalY: 0
44+ property int normalWidth: 0
45+ property int normalHeight: 0
46+
47+ function updateNormalGeometry() {
48+ if (root.target.state == "normal") {
49+ normalX = root.target.x
50+ normalY = root.target.y
51+ normalWidth = root.target.width
52+ normalHeight = root.target.height
53+ }
54+ }
55+ }
56+
57+ Connections {
58+ target: root.target
59+ onXChanged: priv.updateNormalGeometry();
60+ onYChanged: priv.updateNormalGeometry();
61+ onWidthChanged: priv.updateNormalGeometry();
62+ onHeightChanged: priv.updateNormalGeometry();
63+ }
64+
3965 Component.onCompleted: {
40− var windowState = windowStateStorage.getGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
41− if (windowState !== undefined) {
42− target.x = windowState.x
43− target.y = windowState.y
44− target.width = windowState.width
45− target.height = windowState.height
66+ var windowGeometry = windowStateStorage.getGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
67+ if (windowGeometry !== undefined) {
68+ target.x = windowGeometry.x
69+ target.y = windowGeometry.y
70+ target.width = windowGeometry.width
71+ target.height = windowGeometry.height
72+ }
73+ var windowState = windowStateStorage.getState(root.windowId, WindowStateStorage.WindowStateNormal)
74+ if (windowState === WindowStateStorage.WindowStateMaximized) {
75+ target.maximize(false)
4676 }
4777 }
4878
4979 Component.onDestruction: {
50− windowStateStorage.saveGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
80+ windowStateStorage.saveState(root.windowId, target.state == "maximized" ? WindowStateStorage.WindowStateMaximized : WindowStateStorage.WindowStateNormal)
81+ windowStateStorage.saveGeometry(root.windowId, Qt.rect(priv.normalX, priv.normalY, priv.normalWidth, priv.normalHeight))
5182 }
5283
5384 QtObject {
tests/mocks/Utils/windowstatestorage.cpp+17
@@ -44,3 +44,20 @@ QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defa
4444 if (!m_geometry.contains(windowId)) return defaultValue;
4545 return m_geometry.value(windowId).toRect();
4646}
47+
48+void WindowStateStorage::clear()
49+{
50+ m_state.clear();
51+ m_geometry.clear();
52+}
53+
54+void WindowStateStorage::saveState(const QString &windowId, WindowState state)
55+{
56+ m_state[windowId] = state;
57+}
58+
59+WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue)
60+{
61+ if (!m_state.contains(windowId)) return defaultValue;
62+ return m_state.value(windowId);
63+}
tests/mocks/Utils/windowstatestorage.h+12
@@ -22,12 +22,23 @@ class WindowStateStorage: public QObject
2222{
2323 Q_OBJECT
2424 Q_PROPERTY(QVariantMap geometry READ geometry WRITE setGeometry NOTIFY geometryChanged)
25+ Q_ENUMS(WindowState)
2526public:
27+ enum WindowState {
28+ WindowStateNormal,
29+ WindowStateMaximized
30+ };
2631 WindowStateStorage(QObject *parent = 0);
2732
33+ Q_INVOKABLE void saveState(const QString &windowId, WindowState state);
34+ Q_INVOKABLE WindowState getState(const QString &windowId, WindowState defaultValue);
35+
2836 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
2937 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
3038
39+ // Only in the mock, to easily restore a fresh state
40+ Q_INVOKABLE void clear();
41+
3142Q_SIGNALS:
3243 void geometryChanged(const QVariantMap& geometry);
3344
@@ -35,5 +46,6 @@ private:
3546 void setGeometry(const QVariantMap& geometry);
3647 QVariantMap geometry() const;
3748
49+ QHash<QString, WindowState> m_state;
3850 QVariantMap m_geometry;
3951};
tests/qmltests/Stages/tst_DesktopStage.qml+132−25
@@ -53,14 +53,10 @@ Item {
5353
5454 focus: true
5555
56− property bool itemDestroyed: false
5756 sourceComponent: Component {
5857 DesktopStage {
5958 color: "darkblue"
6059 anchors.fill: parent
61− Component.onDestruction: {
62− desktopStageLoader.itemDestroyed = true;
63− }
6460 }
6561 }
6662 }
@@ -95,16 +91,10 @@ Item {
9591 property Item desktopStage: desktopStageLoader.status === Loader.Ready ? desktopStageLoader.item : null
9692
9793 function cleanup() {
98− desktopStageLoader.itemDestroyed = false;
9994 desktopStageLoader.active = false;
10095
10196 tryCompare(desktopStageLoader, "status", Loader.Null);
10297 tryCompare(desktopStageLoader, "item", null);
103− // Loader.status might be Loader.Null and Loader.item might be null but the Loader
104− // actually took place. Likely because Loader waits until the next event loop
105− // iteration to do its work. So to ensure the reload, we will wait until the
106− // Shell instance gets destroyed.
107− tryCompare(desktopStageLoader, "itemDestroyed", true);
10898
10999 killAllRunningApps();
110100
@@ -117,7 +107,7 @@ Item {
117107 var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
118108 ApplicationManager.stopApplication(ApplicationManager.get(appIndex).appId);
119109 }
120− compare(ApplicationManager.count, 1)
110+ compare(ApplicationManager.count, 1);
121111 }
122112
123113 function waitUntilAppSurfaceShowsUp(appId) {
@@ -147,10 +137,7 @@ Item {
147137 }
148138
149139 function test_appFocusSwitch(data) {
150− var i;
151− for (i = 0; i < data.apps.length; i++) {
152− startApplication(data.apps[i]);
153− }
140+ data.apps.forEach(startApplication);
154141
155142 ApplicationManager.requestFocusApplication(data.apps[data.focusfrom]);
156143 tryCompare(ApplicationManager.findApplication(data.apps[data.focusfrom]).session.surface, "activeFocus", true);
@@ -167,10 +154,7 @@ Item {
167154 }
168155
169156 function test_tappingOnWindowChangesFocusedApp(data) {
170− var i;
171− for (i = 0; i < data.apps.length; i++) {
172− startApplication(data.apps[i]);
173− }
157+ data.apps.forEach(startApplication);
174158 var fromAppId = data.apps[data.focusfrom];
175159 var toAppId = data.apps[data.focusTo]
176160
@@ -187,18 +171,37 @@ Item {
187171 compare(ApplicationManager.focusedApplicationId, toAppId);
188172 }
189173
174+ function test_clickingOnWindowChangesFocusedApp_data() {
175+ return test_tappingOnWindowChangesFocusedApp_data(); // reuse test data
176+ }
177+
178+ function test_clickingOnWindowChangesFocusedApp(data) {
179+ data.apps.forEach(startApplication);
180+ var fromAppId = data.apps[data.focusfrom];
181+ var toAppId = data.apps[data.focusTo]
182+
183+ var fromAppWindow = findChild(desktopStage, "appWindow_" + fromAppId);
184+ verify(fromAppWindow);
185+ mouseClick(fromAppWindow);
186+ compare(fromAppWindow.application.session.surface.activeFocus, true);
187+ compare(ApplicationManager.focusedApplicationId, fromAppId);
188+
189+ var toAppWindow = findChild(desktopStage, "appWindow_" + toAppId);
190+ verify(toAppWindow);
191+ mouseClick(toAppWindow);
192+ compare(toAppWindow.application.session.surface.activeFocus, true);
193+ compare(ApplicationManager.focusedApplicationId, toAppId);
194+ }
195+
190196 function test_tappingOnDecorationFocusesApplication_data() {
191197 return [
192− {tag: "dash", apps: [ "unity8-dash", "dialer-app", "camera-app" ], focusfrom: 0, focusTo: 1 },
193− {tag: "dash", apps: [ "unity8-dash", "dialer-app", "camera-app" ], focusfrom: 1, focusTo: 0 },
198+ {tag: "dash to dialer", apps: [ "unity8-dash", "dialer-app", "camera-app" ], focusfrom: 0, focusTo: 1 },
199+ {tag: "dialer to dash", apps: [ "unity8-dash", "dialer-app", "camera-app" ], focusfrom: 1, focusTo: 0 },
194200 ]
195201 }
196202
197203 function test_tappingOnDecorationFocusesApplication(data) {
198− var i;
199− for (i = 0; i < data.apps.length; i++) {
200− startApplication(data.apps[i]);
201− }
204+ data.apps.forEach(startApplication);
202205
203206 var fromAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusfrom]);
204207 verify(fromAppDecoration);
@@ -210,5 +213,109 @@ Item {
210213 tap(toAppDecoration);
211214 tryCompare(ApplicationManager.findApplication(data.apps[data.focusTo]).session.surface, "activeFocus", true);
212215 }
216+
217+ function test_clickingOnDecorationFocusesApplication_data() {
218+ return test_tappingOnDecorationFocusesApplication_data(); // reuse test data
219+ }
220+
221+ function test_clickingOnDecorationFocusesApplication(data) {
222+ data.apps.forEach(startApplication);
223+
224+ var fromAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusfrom]);
225+ verify(fromAppDecoration);
226+ mouseClick(fromAppDecoration);
227+ tryCompare(ApplicationManager.findApplication(data.apps[data.focusfrom]).session.surface, "activeFocus", true);
228+
229+ var toAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusTo]);
230+ verify(toAppDecoration);
231+ mouseClick(toAppDecoration);
232+ tryCompare(ApplicationManager.findApplication(data.apps[data.focusTo]).session.surface, "activeFocus", true);
233+ }
234+
235+ function test_windowMaximize() {
236+ var apps = ["unity8-dash", "dialer-app", "camera-app"];
237+ apps.forEach(startApplication);
238+ var appName = "dialer-app";
239+ var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
240+ verify(appDelegate);
241+ ApplicationManager.focusApplication(appName);
242+ keyClick(Qt.Key_Up, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Up shortcut to maximize
243+ tryCompare(appDelegate, "maximized", true);
244+ tryCompare(appDelegate, "minimized", false);
245+ }
246+
247+ function test_windowMaximizeLeft() {
248+ var apps = ["unity8-dash", "dialer-app", "camera-app"];
249+ apps.forEach(startApplication);
250+ var appName = "dialer-app";
251+ var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
252+ verify(appDelegate);
253+ ApplicationManager.focusApplication(appName);
254+ keyClick(Qt.Key_Left, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Left shortcut to maximizeLeft
255+ tryCompare(appDelegate, "maximized", false);
256+ tryCompare(appDelegate, "minimized", false);
257+ tryCompare(appDelegate, "maximizedLeft", true);
258+ tryCompare(appDelegate, "maximizedRight", false);
259+ }
260+
261+ function test_windowMaximizeRight() {
262+ var apps = ["unity8-dash", "dialer-app", "camera-app"];
263+ apps.forEach(startApplication);
264+ var appName = "dialer-app";
265+ var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
266+ verify(appDelegate);
267+ ApplicationManager.focusApplication(appName);
268+ keyClick(Qt.Key_Right, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Right shortcut to maximizeRight
269+ tryCompare(appDelegate, "maximized", false);
270+ tryCompare(appDelegate, "minimized", false);
271+ tryCompare(appDelegate, "maximizedLeft", false);
272+ tryCompare(appDelegate, "maximizedRight", true);
273+ }
274+
275+ function test_windowMinimize() {
276+ var apps = ["unity8-dash", "dialer-app", "camera-app"];
277+ apps.forEach(startApplication);
278+ var appName = "dialer-app";
279+ var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
280+ verify(appDelegate);
281+ ApplicationManager.focusApplication(appName);
282+ keyClick(Qt.Key_Down, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Down shortcut to minimize
283+ tryCompare(appDelegate, "maximized", false);
284+ tryCompare(appDelegate, "minimized", true);
285+ verify(ApplicationManager.focusedApplicationId != ""); // verify we don't lose focus when minimizing an app
286+ }
287+
288+ function test_windowMinimizeAll() {
289+ var apps = ["unity8-dash", "dialer-app", "camera-app"];
290+ apps.forEach(startApplication);
291+ verify(ApplicationManager.count == 3);
292+ keyClick(Qt.Key_D, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+D shortcut to minimize all
293+ tryCompare(ApplicationManager, "focusedApplicationId", ""); // verify no app is focused
294+ }
295+
296+ function test_windowClose() {
297+ var apps = ["unity8-dash", "dialer-app", "camera-app"];
298+ apps.forEach(startApplication);
299+ verify(ApplicationManager.count == 3);
300+ var appName = "dialer-app";
301+ var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
302+ verify(appDelegate);
303+ ApplicationManager.focusApplication(appName);
304+ keyClick(Qt.Key_F4, Qt.AltModifier); // Alt+F4 shortcut to close
305+ verify(ApplicationManager.count == 2); // verify the app is gone
306+ verify(ApplicationManager.findApplication(appName) === null); // and it's not in running apps
307+ }
308+
309+ function test_smashCursorKeys() {
310+ var apps = ["unity8-dash", "dialer-app", "camera-app"];
311+ apps.forEach(startApplication);
312+ verify(ApplicationManager.count == 3);
313+ keyClick(Qt.Key_D, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+D shortcut to minimize all
314+ tryCompare(ApplicationManager, "focusedApplicationId", ""); // verify no app is focused
315+
316+ // now try pressing all 4 arrow keys + ctrl + meta
317+ keyClick(Qt.Key_Up | Qt.Key_Down | Qt.Key_Left | Qt.Key_Right, Qt.MetaModifier|Qt.ControlModifier); // smash it!!!
318+ tryCompare(ApplicationManager, "focusedApplicationId", ""); // verify still no app is focused
319+ }
213320 }
214321}
tests/qmltests/Stages/tst_WindowResizeArea.qml+35−2
@@ -44,8 +44,11 @@ Item {
4444 width: units.gu(20)
4545 property int windowHeight: height
4646 property int windowWidth: width
47− onWindowHeightChanged: height = windowHeight
48− onWindowWidthChanged: width = windowWidth
47+ state: "normal"
48+
49+ function maximize() {
50+ state = "maximized"
51+ }
4952
5053 WindowResizeArea {
5154 id: windowResizeArea
@@ -195,5 +198,35 @@ Item {
195198 tryCompare(fakeWindow, "width", initialWindowWidth);
196199 tryCompare(fakeWindow, "height", initialWindowHeight);
197200 }
201+
202+ function test_saveRestoreMaximized() {
203+ var initialWindowX = fakeWindow.x;
204+ var initialWindowY = fakeWindow.y;
205+
206+ var moveDelta = units.gu(5);
207+
208+ fakeWindow.x = initialWindowX + moveDelta
209+ fakeWindow.y = initialWindowY + moveDelta
210+
211+ // Now change the state to maximized. The window should not keep updating the stored values
212+ fakeWindow.state = "maximized"
213+ fakeWindow.x = 31415 // 0 is too risky to pass the test even when broken
214+ fakeWindow.y = 31415
215+
216+ // This will destroy the window and recreate it
217+ windowLoader.active = false;
218+ waitForRendering(root);
219+ windowLoader.active = true;
220+
221+ // Make sure it's again where we left it in normal state before destroying
222+ tryCompare(fakeWindow, "x", initialWindowX + moveDelta)
223+ tryCompare(fakeWindow, "y", initialWindowX + moveDelta)
224+
225+ // Make sure maximize() has been called after restoring
226+ tryCompare(fakeWindow, "state", "maximized")
227+
228+ // clean up
229+ fakeWindow.state = "normal"
230+ }
198231 }
199232}
tests/qmltests/tst_Shell.qml+73
@@ -29,8 +29,10 @@ import Unity.Notifications 1.0
2929import Unity.Test 0.1
3030import Powerd 0.1
3131import Wizard 0.1 as Wizard
32+import Utils 0.1
3233
3334import "../../qml"
35+import "../../qml/Components/PanelState"
3436import "Stages"
3537
3638Rectangle {
@@ -302,6 +304,7 @@ Rectangle {
302304 mouseEmulation.checked = true;
303305 tryCompare(shell, "enabled", true); // make sure greeter didn't leave us in disabled state
304306 tearDown();
307+ WindowStateStorage.clear();
305308 }
306309
307310 function loadShell(formFactor) {
@@ -1707,5 +1710,75 @@ Rectangle {
17071710
17081711 keyRelease(Qt.Key_Control);
17091712 }
1713+
1714+ // regression test for http://pad.lv/1443319
1715+ function test_closeMaximizedAndRestart() {
1716+ loadDesktopShellWithApps();
1717+
1718+ var appRepeater = findChild(shell, "appRepeater")
1719+ var appId = ApplicationManager.get(0).appId;
1720+ var appDelegate = appRepeater.itemAt(0);
1721+ var maximizeButton = findChild(appDelegate, "maximizeWindowButton");
1722+
1723+ wait(5000)
1724+ tryCompare(appDelegate, "state", "normal");
1725+ tryCompare(PanelState, "buttonsVisible", false)
1726+
1727+ wait(5000)
1728+ mouseClick(maximizeButton, maximizeButton.width / 2, maximizeButton.height / 2);
1729+ tryCompare(appDelegate, "state", "maximized");
1730+ tryCompare(PanelState, "buttonsVisible", true)
1731+
1732+ ApplicationManager.stopApplication(appId);
1733+ tryCompare(PanelState, "buttonsVisible", false)
1734+
1735+ ApplicationManager.startApplication(appId);
1736+ tryCompare(PanelState, "buttonsVisible", true)
1737+ }
1738+
1739+ // bug http://pad.lv/1431566
1740+ function test_switchToStagedHidesPanelButtons() {
1741+ loadDesktopShellWithApps();
1742+ var appRepeater = findChild(shell, "appRepeater")
1743+ var appId = ApplicationManager.get(0).appId;
1744+ var appDelegate = appRepeater.itemAt(0);
1745+ var panelButtons = findChild(shell, "panelWindowControlButtons")
1746+
1747+ tryCompare(appDelegate, "state", "normal");
1748+ tryCompare(panelButtons, "visible", false);
1749+
1750+ appDelegate.maximize(false);
1751+ tryCompare(panelButtons, "visible", true);
1752+
1753+ shell.usageScenario = "phone";
1754+ waitForRendering(shell);
1755+ tryCompare(panelButtons, "visible", false);
1756+
1757+ shell.usageScenario = "desktop";
1758+ waitForRendering(shell);
1759+ tryCompare(panelButtons, "visible", true);
1760+ }
1761+
1762+ function test_lockingGreeterHidesPanelButtons() {
1763+ loadDesktopShellWithApps();
1764+ var appRepeater = findChild(shell, "appRepeater")
1765+ var appId = ApplicationManager.get(0).appId;
1766+ var appDelegate = appRepeater.itemAt(0);
1767+ var panelButtons = findChild(shell, "panelWindowControlButtons")
1768+
1769+ tryCompare(appDelegate, "state", "normal");
1770+ tryCompare(panelButtons, "visible", false);
1771+
1772+ appDelegate.maximize(false);
1773+ tryCompare(panelButtons, "visible", true);
1774+
1775+ LightDM.Greeter.showGreeter();
1776+ waitForRendering(shell);
1777+ tryCompare(panelButtons, "visible", false);
1778+
1779+ LightDM.Greeter.hideGreeter();
1780+ waitForRendering(shell);
1781+ tryCompare(panelButtons, "visible", true);
1782+ }
17101783 }
17111784}