← Back to team overview

ubuntu-touch-coreapps-reviewers team mailing list archive

[Merge] lp:~fboucault/ubuntu-terminal-app/tiled_view into lp:ubuntu-terminal-app

 

Florian Boucault has proposed merging lp:~fboucault/ubuntu-terminal-app/tiled_view into lp:ubuntu-terminal-app.

Commit message:
New feature: terminals can be organised as tiles.

Requested reviews:
  Ubuntu Terminal Developers (ubuntu-terminal-dev)

For more details, see:
https://code.launchpad.net/~fboucault/ubuntu-terminal-app/tiled_view/+merge/314617

New feature: terminals can be organised as tiles.
-- 
Your team Ubuntu Terminal Developers is requested to review the proposed merge of lp:~fboucault/ubuntu-terminal-app/tiled_view into lp:ubuntu-terminal-app.
=== modified file 'po/com.ubuntu.terminal.pot'
--- po/com.ubuntu.terminal.pot	2017-01-06 08:39:01 +0000
+++ po/com.ubuntu.terminal.pot	2017-01-12 13:26:17 +0000
@@ -8,7 +8,7 @@
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-01-06 08:38+0000\n"
+"POT-Creation-Date: 2017-01-12 13:40+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@xxxxxx>\n"
@@ -29,15 +29,23 @@
 msgid "Paste"
 msgstr ""
 
-#: ../src/app/qml/AlternateActionPopover.qml:86 ../src/app/qml/TabsPage.qml:32
+#: ../src/app/qml/AlternateActionPopover.qml:86
+msgid "Split horizontally"
+msgstr ""
+
+#: ../src/app/qml/AlternateActionPopover.qml:92
+msgid "Split vertically"
+msgstr ""
+
+#: ../src/app/qml/AlternateActionPopover.qml:99 ../src/app/qml/TabsPage.qml:32
 msgid "New tab"
 msgstr ""
 
-#: ../src/app/qml/AlternateActionPopover.qml:91
+#: ../src/app/qml/AlternateActionPopover.qml:104
 msgid "New window"
 msgstr ""
 
-#: ../src/app/qml/AlternateActionPopover.qml:96
+#: ../src/app/qml/AlternateActionPopover.qml:109
 msgid "Close"
 msgstr ""
 
@@ -303,7 +311,7 @@
 msgid "Tabs"
 msgstr ""
 
-#: ../src/app/qml/TerminalPage.qml:252
+#: ../src/app/qml/TerminalPage.qml:132
 msgid "Selection Mode"
 msgstr ""
 

=== modified file 'src/app/qml/AlternateActionPopover.qml'
--- src/app/qml/AlternateActionPopover.qml	2017-01-06 08:38:45 +0000
+++ src/app/qml/AlternateActionPopover.qml	2017-01-12 13:26:17 +0000
@@ -83,6 +83,19 @@
             property bool divider: true
         }
         Action {
+            text: i18n.tr("Split horizontally")
+            onTriggered: tiledTerminalView.splitTerminal(terminal, Qt.Vertical)
+            shortcut: settings.shortcutSplitHorizontally
+            enabled: terminal.height >= 2 * tiledTerminalView.minimumTileHeight
+        }
+        Action {
+            text: i18n.tr("Split vertically")
+            onTriggered: tiledTerminalView.splitTerminal(terminal, Qt.Horizontal)
+            shortcut: settings.shortcutSplitVertically
+            enabled: terminal.width >= 2 * tiledTerminalView.minimumTileWidth
+            property bool divider: true
+        }
+        Action {
             text: i18n.tr("New tab")
             onTriggered: tabsModel.addTerminalTab()
             shortcut: settings.shortcutNewTab
@@ -94,7 +107,7 @@
         }
         Action {
             text: i18n.tr("Close")
-            onTriggered: tabsModel.removeItem(tabsModel.indexOf(tabsModel.currentItem))
+            onTriggered: terminal.finished()
             shortcut: settings.shortcutCloseTab
         }
     }

=== modified file 'src/app/qml/Settings/SettingsShortcutsSection.qml'
--- src/app/qml/Settings/SettingsShortcutsSection.qml	2016-11-30 22:56:25 +0000
+++ src/app/qml/Settings/SettingsShortcutsSection.qml	2017-01-12 13:26:17 +0000
@@ -186,6 +186,36 @@
                         actionLabel: QT_TR_NOOP("Toggle fullscreen")
                         shortcutSetting: "shortcutFullscreen"
                     }
+                    ListElement {
+                        section: QT_TR_NOOP("View")
+                        actionLabel: QT_TR_NOOP("Split terminal horizontally")
+                        shortcutSetting: "shortcutSplitHorizontally"
+                    }
+                    ListElement {
+                        section: QT_TR_NOOP("View")
+                        actionLabel: QT_TR_NOOP("Split terminal vertically")
+                        shortcutSetting: "shortcutSplitVertically"
+                    }
+                    ListElement {
+                        section: QT_TR_NOOP("View")
+                        actionLabel: QT_TR_NOOP("Navigate to terminal above")
+                        shortcutSetting: "shortcutMoveToTileAbove"
+                    }
+                    ListElement {
+                        section: QT_TR_NOOP("View")
+                        actionLabel: QT_TR_NOOP("Navigate to terminal below")
+                        shortcutSetting: "shortcutMoveToTileBelow"
+                    }
+                    ListElement {
+                        section: QT_TR_NOOP("View")
+                        actionLabel: QT_TR_NOOP("Navigate to terminal on the left")
+                        shortcutSetting: "shortcutMoveToTileLeft"
+                    }
+                    ListElement {
+                        section: QT_TR_NOOP("View")
+                        actionLabel: QT_TR_NOOP("Navigate to terminal on the right")
+                        shortcutSetting: "shortcutMoveToTileRight"
+                    }
                 }
 
                 delegate: ShortcutRow {

=== modified file 'src/app/qml/TabsModel.qml'
--- src/app/qml/TabsModel.qml	2016-12-12 21:18:17 +0000
+++ src/app/qml/TabsModel.qml	2017-01-12 13:26:17 +0000
@@ -34,17 +34,18 @@
         moveItem(from, to);
     }
 
-    property Component terminalComponent: TerminalComponent {}
+    property Component tiledViewComponent: TiledTerminalView {}
 
     function addTerminalTab(initialWorkingDirectory) {
         if (currentItem) {
-            initialWorkingDirectory = currentItem.session.getWorkingDirectory();
+            initialWorkingDirectory = currentItem.focusedTerminal.session.getWorkingDirectory();
         }
 
-        var termObject = terminalComponent.createObject(terminalPage.terminalContainer,
+        var tiledView = tiledViewComponent.createObject(terminalPage.terminalContainer,
                                                         {"initialWorkingDirectory": initialWorkingDirectory,
-                                                         "visible": Qt.binding(function () { return tabsModel.currentItem === termObject})});
-        tabsModel.addItem(termObject);
+                                                         "visible": Qt.binding(function () { return tabsModel.currentItem === tiledView})});
+        tiledView.emptied.connect(function () {tabsModel.removeItem(tabsModel.indexOf(tiledView));})
+        tabsModel.addItem(tiledView);
         currentIndex = tabsModel.count - 1;
     }
 

=== modified file 'src/app/qml/TabsPage.qml'
--- src/app/qml/TabsPage.qml	2016-12-12 12:44:22 +0000
+++ src/app/qml/TabsPage.qml	2017-01-12 13:26:17 +0000
@@ -103,7 +103,7 @@
                     Label {
                         anchors { fill: blackRect; margins: units.dp(2) }
                         property var tab: tabsModel.itemAt(index)
-                        text: tab ? tab.session.title : ""
+                        text: tab && tab.focusedTerminal ? tab.focusedTerminal.session.title : ""
                         wrapMode: Text.Wrap
                         color: "white"
                     }

=== renamed file 'src/app/qml/TerminalComponent.qml' => 'src/app/qml/Terminal.qml'
--- src/app/qml/TerminalComponent.qml	2016-12-12 12:44:22 +0000
+++ src/app/qml/Terminal.qml	2017-01-12 13:26:17 +0000
@@ -17,20 +17,21 @@
  */
 import QtQuick 2.4
 import Ubuntu.Components 1.3
+import Ubuntu.Components.Popups 1.3
 import QMLTermWidget 1.0
 import Terminal 0.1
 
 QMLTermWidget {
     id: terminal
-    width: parent.width
-    height: parent.height
 
     colorScheme: settings.colorScheme
     font.family: settings.fontStyle
     font.pixelSize: FontUtils.sizeToPixels("medium") * settings.fontSize / 10
 
+    property bool isDarkBackground: ColorUtils.luminance(backgroundColor) <= 0.85
+    property color contourColor: isDarkBackground ? Qt.rgba(1.0, 1.0, 1.0, 0.4) : Qt.rgba(0.0, 0.0, 0.0, 0.2)
     property string initialWorkingDirectory
-    signal sessionFinished(var session);
+    signal finished()
 
     session: QMLTermSession {
         id: terminalSession
@@ -68,7 +69,7 @@
              "-o", "LogLevel=Error",
              "mkdir -p `dirname %1`; echo -n $$ > %1; cd %2; bash".arg(sshShellPidFile).arg(initialWorkingDirectory)]
             : [])
-        onFinished: tabsModel.removeItem(tabsModel.indexOf(terminal))
+        onFinished: terminal.finished()
     }
 
     property int totalLines: terminal.scrollbarMaximum - terminal.scrollbarMinimum + terminal.lines
@@ -77,4 +78,116 @@
         terminalSession.startShellProgram();
         forceActiveFocus();
     }
+
+    // TODO: This invisible button is used to position the popover where the
+    // alternate action was called. Terrible terrible workaround!
+    Item {
+        id: hiddenButton
+        width: 1
+        height: 1
+        visible: false
+        enabled: false
+    }
+
+    TerminalInputArea {
+        id: inputArea
+        enabled: terminalPage.state != "SELECTION"
+        anchors.fill: parent
+        // FIXME: should anchor to the bottom of the window to cater for the case when the OSK is up
+
+        // This is the minimum wheel event registered by the plugin (with the current settings).
+        property real wheelValue: 40
+
+        // This is needed to fake a "flickable" scrolling.
+        swipeDelta: terminal.fontMetrics.height
+
+        // Mouse actions
+        onMouseMoveDetected: terminal.simulateMouseMove(x, y, button, buttons, modifiers);
+        onDoubleClickDetected: terminal.simulateMouseDoubleClick(x, y, button, buttons, modifiers);
+        onMousePressDetected: {
+            terminal.forceActiveFocus();
+            terminal.simulateMousePress(x, y, button, buttons, modifiers);
+        }
+        onMouseReleaseDetected: terminal.simulateMouseRelease(x, y, button, buttons, modifiers);
+        onMouseWheelDetected: terminal.simulateWheel(x, y, buttons, modifiers, angleDelta);
+
+        // Touch actions
+        onTouchPress: terminal.forceActiveFocus()
+        onTouchClick: terminal.simulateKeyPress(Qt.Key_Tab, Qt.NoModifier, true, 0, "");
+        onTouchPressAndHold: alternateAction(x, y);
+
+        // Swipe actions
+        onSwipeYDetected: {
+            if (steps > 0) {
+                simulateSwipeDown(steps);
+            } else {
+                simulateSwipeUp(-steps);
+            }
+        }
+        onSwipeXDetected: {
+            if (steps > 0) {
+                simulateSwipeRight(steps);
+            } else {
+                simulateSwipeLeft(-steps);
+            }
+        }
+        onTwoFingerSwipeYDetected: {
+            if (steps > 0) {
+                simulateDualSwipeDown(steps);
+            } else {
+                simulateDualSwipeUp(-steps);
+            }
+        }
+
+        function simulateSwipeUp(steps) {
+            while(steps > 0) {
+                terminal.simulateKeyPress(Qt.Key_Up, Qt.NoModifier, true, 0, "");
+                steps--;
+            }
+        }
+        function simulateSwipeDown(steps) {
+            while(steps > 0) {
+                terminal.simulateKeyPress(Qt.Key_Down, Qt.NoModifier, true, 0, "");
+                steps--;
+            }
+        }
+        function simulateSwipeLeft(steps) {
+            while(steps > 0) {
+                terminal.simulateKeyPress(Qt.Key_Left, Qt.NoModifier, true, 0, "");
+                steps--;
+            }
+        }
+        function simulateSwipeRight(steps) {
+            while(steps > 0) {
+                terminal.simulateKeyPress(Qt.Key_Right, Qt.NoModifier, true, 0, "");
+                steps--;
+            }
+        }
+        function simulateDualSwipeUp(steps) {
+            while(steps > 0) {
+                terminal.simulateWheel(width * 0.5, height * 0.5, Qt.NoButton, Qt.NoModifier, Qt.point(0, -wheelValue));
+                steps--;
+            }
+        }
+        function simulateDualSwipeDown(steps) {
+            while(steps > 0) {
+                terminal.simulateWheel(width * 0.5, height * 0.5, Qt.NoButton, Qt.NoModifier, Qt.point(0, wheelValue));
+                steps--;
+            }
+        }
+
+        // Semantic actions
+        onAlternateAction: {
+            // Force the hiddenButton in the event position.
+            hiddenButton.x = x;
+            hiddenButton.y = y;
+            PopupUtils.open(Qt.resolvedUrl("AlternateActionPopover.qml"),
+                            hiddenButton);
+        }
+    }
+
+    QMLTermScrollbar {
+        anchors.fill: parent
+        terminal: terminal
+    }
 }

=== modified file 'src/app/qml/TerminalPage.qml'
--- src/app/qml/TerminalPage.qml	2017-01-06 08:37:39 +0000
+++ src/app/qml/TerminalPage.qml	2017-01-12 13:26:17 +0000
@@ -27,7 +27,7 @@
 Page {
     id: terminalPage
     property alias terminalContainer: terminalContainer
-    property Item terminal
+    property Terminal terminal
     property var tabsModel
     property bool narrowLayout
     theme: ThemeSettings {
@@ -50,15 +50,15 @@
             top: parent.top
             right: parent.right
         }
-        property bool isDarkBackground: ColorUtils.luminance(backgroundColor) <= 0.85
+        property bool isDarkBackground: terminalPage.terminal && terminalPage.terminal.isDarkBackground
         actionColor: isDarkBackground ? "white" : "black"
         backgroundColor: terminalPage.terminal ? terminalPage.terminal.backgroundColor : ""
         foregroundColor: terminalPage.terminal ? terminalPage.terminal.foregroundColor : ""
-        contourColor: isDarkBackground ? Qt.rgba(1.0, 1.0, 1.0, 0.4) : Qt.rgba(0.0, 0.0, 0.0, 0.2)
+        contourColor: terminalPage.terminal ? terminalPage.terminal.contourColor : ""
         color: isDarkBackground ? Qt.tint(backgroundColor, "#0DFFFFFF") : Qt.tint(backgroundColor, "#0D000000")
         model: terminalPage.tabsModel
         function titleFromModelItem(modelItem) {
-            return modelItem.session.title;
+            return modelItem.focusedTerminal ? modelItem.focusedTerminal.session.title : "";
         }
 
         actions: [
@@ -84,7 +84,6 @@
             top: terminalPage.narrowLayout ? parent.top : tabsBar.bottom;
             right: parent.right;
             bottom: keyboardBarLoader.top
-            margins: units.gu(1)
         }
 
         Binding {
@@ -94,125 +93,6 @@
         }
     }
 
-    QMLTermScrollbar {
-        anchors {
-            top: terminalContainer.anchors.top
-            bottom: terminalContainer.anchors.bottom
-            left: terminalContainer.anchors.left
-            right: terminalContainer.anchors.right
-        }
-
-        terminal: terminalPage.terminal
-        z: inputArea.z + 1
-    }
-
-    // TODO: This invisible button is used to position the popover where the
-    // alternate action was called. Terrible terrible workaround!
-    Button {
-        id: hiddenButton
-        width: 5
-        height: 5
-        visible: false
-        enabled: false
-    }
-
-    TerminalInputArea{
-        id: inputArea
-        anchors {
-            left: terminalContainer.anchors.left
-            top: terminalContainer.anchors.top
-            right: terminalContainer.anchors.right
-            bottom: parent.bottom
-            margins: terminalContainer.anchors.margins
-        }
-        enabled: terminal
-
-        // This is the minimum wheel event registered by the plugin (with the current settings).
-        property real wheelValue: 40
-
-        // This is needed to fake a "flickable" scrolling.
-        swipeDelta: terminal ? terminal.fontMetrics.height : 0
-
-        // Mouse actions
-        onMouseMoveDetected: terminal.simulateMouseMove(x, y, button, buttons, modifiers);
-        onDoubleClickDetected: terminal.simulateMouseDoubleClick(x, y, button, buttons, modifiers);
-        onMousePressDetected: terminal.simulateMousePress(x, y, button, buttons, modifiers);
-        onMouseReleaseDetected: terminal.simulateMouseRelease(x, y, button, buttons, modifiers);
-        onMouseWheelDetected: terminal.simulateWheel(x, y, buttons, modifiers, angleDelta);
-
-        // Touch actions
-        onTouchClick: terminal.simulateKeyPress(Qt.Key_Tab, Qt.NoModifier, true, 0, "");
-        onTouchPressAndHold: alternateAction(x, y);
-
-        // Swipe actions
-        onSwipeYDetected: {
-            if (steps > 0) {
-                simulateSwipeDown(steps);
-            } else {
-                simulateSwipeUp(-steps);
-            }
-        }
-        onSwipeXDetected: {
-            if (steps > 0) {
-                simulateSwipeRight(steps);
-            } else {
-                simulateSwipeLeft(-steps);
-            }
-        }
-        onTwoFingerSwipeYDetected: {
-            if (steps > 0) {
-                simulateDualSwipeDown(steps);
-            } else {
-                simulateDualSwipeUp(-steps);
-            }
-        }
-
-        function simulateSwipeUp(steps) {
-            while(steps > 0) {
-                terminal.simulateKeyPress(Qt.Key_Up, Qt.NoModifier, true, 0, "");
-                steps--;
-            }
-        }
-        function simulateSwipeDown(steps) {
-            while(steps > 0) {
-                terminal.simulateKeyPress(Qt.Key_Down, Qt.NoModifier, true, 0, "");
-                steps--;
-            }
-        }
-        function simulateSwipeLeft(steps) {
-            while(steps > 0) {
-                terminal.simulateKeyPress(Qt.Key_Left, Qt.NoModifier, true, 0, "");
-                steps--;
-            }
-        }
-        function simulateSwipeRight(steps) {
-            while(steps > 0) {
-                terminal.simulateKeyPress(Qt.Key_Right, Qt.NoModifier, true, 0, "");
-                steps--;
-            }
-        }
-        function simulateDualSwipeUp(steps) {
-            while(steps > 0) {
-                terminal.simulateWheel(width * 0.5, height * 0.5, Qt.NoButton, Qt.NoModifier, Qt.point(0, -wheelValue));
-                steps--;
-            }
-        }
-        function simulateDualSwipeDown(steps) {
-            while(steps > 0) {
-                terminal.simulateWheel(width * 0.5, height * 0.5, Qt.NoButton, Qt.NoModifier, Qt.point(0, wheelValue));
-                steps--;
-            }
-        }
-
-        // Semantic actions
-        onAlternateAction: {
-            // Force the hiddenButton in the event position.
-            hiddenButton.x = x;
-            hiddenButton.y = y;
-            PopupUtils.open(Qt.resolvedUrl("AlternateActionPopover.qml"), hiddenButton);
-        }
-    }
-
     Loader {
         id: keyboardBarLoader
         height: active ? units.gu(5) : 0
@@ -227,7 +107,7 @@
             backgroundColor: tabsBar.color
             foregroundColor: tabsBar.foregroundColor
             onSimulateKey: terminal.simulateKeyPress(key, mod, true, 0, "");
-            onSimulateCommand: terminal.session.sendText(command);
+            onSimulateCommand: terminal.focusedTerminal.session.sendText(command);
         }
     }
 
@@ -267,7 +147,7 @@
             iconName: "close"
             onTriggered: {
                 terminalPage.state = "DEFAULT";
-                PopupUtils.open(Qt.resolvedUrl("AlternateActionPopover.qml"), hiddenButton);
+                PopupUtils.open(Qt.resolvedUrl("AlternateActionPopover.qml"));
             }
         }
     }
@@ -333,7 +213,6 @@
             PropertyChanges { target: keyboardButton; visible: false }
             PropertyChanges { target: bottomMessage; active: true }
             PropertyChanges { target: keyboardBarLoader; enabled: false }
-            PropertyChanges { target: inputArea; enabled: false }
         }
     ]
 }

=== modified file 'src/app/qml/TerminalSettings.qml'
--- src/app/qml/TerminalSettings.qml	2016-11-30 09:17:02 +0000
+++ src/app/qml/TerminalSettings.qml	2017-01-12 13:26:17 +0000
@@ -34,6 +34,12 @@
     property alias shortcutCopy: innerSettings.shortcutCopy
     property alias shortcutPaste: innerSettings.shortcutPaste
     property alias shortcutFullscreen: innerSettings.shortcutFullscreen
+    property alias shortcutSplitHorizontally: innerSettings.shortcutSplitHorizontally
+    property alias shortcutSplitVertically: innerSettings.shortcutSplitVertically
+    property alias shortcutMoveToTileAbove: innerSettings.shortcutMoveToTileAbove
+    property alias shortcutMoveToTileBelow: innerSettings.shortcutMoveToTileBelow
+    property alias shortcutMoveToTileLeft: innerSettings.shortcutMoveToTileLeft
+    property alias shortcutMoveToTileRight: innerSettings.shortcutMoveToTileRight
 
     readonly property int defaultFontSize: 12
     readonly property int minFontSize: 4
@@ -73,6 +79,12 @@
         property string shortcutCopy: "Ctrl+Shift+C"
         property string shortcutPaste: "Ctrl+Shift+V"
         property string shortcutFullscreen: "F11"
+        property string shortcutSplitHorizontally: "Ctrl+Shift+E"
+        property string shortcutSplitVertically: "Ctrl+Shift+O"
+        property string shortcutMoveToTileAbove: "Alt+Up"
+        property string shortcutMoveToTileBelow: "Alt+Down"
+        property string shortcutMoveToTileLeft: "Alt+Left"
+        property string shortcutMoveToTileRight: "Alt+Right"
     }
 
     // Load the keyboard profiles.

=== modified file 'src/app/qml/TerminalWindow.qml'
--- src/app/qml/TerminalWindow.qml	2016-12-12 13:41:58 +0000
+++ src/app/qml/TerminalWindow.qml	2017-01-12 13:26:17 +0000
@@ -25,7 +25,7 @@
 Window {
     id: terminalWindow
 
-    title: tabsModel.currentItem ? tabsModel.currentItem.session.title : ""
+    title: tabsModel.currentItem && tabsModel.currentItem.focusedTerminal ? tabsModel.currentItem.focusedTerminal.session.title : ""
     color: terminalPage.active && terminalPage.terminal ? terminalPage.terminal.backgroundColor : theme.palette.selected.overlay
     contentOrientation: Screen.orientation
 
@@ -35,7 +35,7 @@
     Binding {
         target: terminalAppRoot
         property: "focusedTerminal"
-        value: tabsModel.currentItem
+        value: tabsModel.currentItem ? tabsModel.currentItem.focusedTerminal : null
         when: terminalWindow.active
     }
 
@@ -71,6 +71,21 @@
                         }
     }
 
+    property TiledTerminalView tiledTerminalView: tabsModel.currentItem
+    Shortcut {
+        sequence: settings.shortcutSplitVertically
+        onActivated: tiledTerminalView.splitTerminal(tiledTerminalView.focusedTerminal,
+                                                     Qt.Horizontal)
+        enabled: tiledTerminalView.focusedTerminal.width >= 2 * tiledTerminalView.minimumTileWidth
+    }
+
+    Shortcut {
+        sequence: settings.shortcutSplitHorizontally
+        onActivated: tiledTerminalView.splitTerminal(tiledTerminalView.focusedTerminal,
+                                                     Qt.Vertical)
+        enabled: tiledTerminalView.focusedTerminal.height >= 2 * tiledTerminalView.minimumTileHeight
+    }
+
     Shortcut {
         sequence: settings.shortcutNewTab
         onActivated: tabsModel.addTerminalTab()
@@ -98,12 +113,12 @@
 
     Shortcut {
         sequence: settings.shortcutCopy
-        onActivated: tabsModel.currentItem.copyClipboard()
+        onActivated: tabsModel.currentItem.focusedTerminal.copyClipboard()
     }
 
     Shortcut {
         sequence: settings.shortcutPaste
-        onActivated: tabsModel.currentItem.pasteClipboard()
+        onActivated: tabsModel.currentItem.focusedTerminal.pasteClipboard()
     }
 
     Shortcut {
@@ -141,7 +156,7 @@
         TerminalPage {
             id: terminalPage
             tabsModel: tabsModel
-            terminal: tabsModel.currentItem
+            terminal: tabsModel.currentItem ? tabsModel.currentItem.focusedTerminal : null
             narrowLayout: terminalWindow.narrowLayout
             // Hide terminal data when the access is still not granted
             layer.enabled: authService.isDialogVisible

=== added file 'src/app/qml/TiledTerminalView.qml'
--- src/app/qml/TiledTerminalView.qml	1970-01-01 00:00:00 +0000
+++ src/app/qml/TiledTerminalView.qml	2017-01-12 13:26:17 +0000
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016-2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored-by: Florian Boucault <florian.boucault@xxxxxxxxxxxxx>
+ */
+import QtQuick 2.5
+import Ubuntu.Components 1.3
+
+TiledView {
+    id: tiledTerminalView
+    anchors.fill: parent
+
+    property string initialWorkingDirectory
+    property Terminal focusedTerminal
+    signal emptied
+    onCountChanged: if (count == 0) emptied()
+
+    function splitTerminal(terminal, orientation) {
+        var initialWorkingDirectory = focusedTerminal.session.getWorkingDirectory();
+        var newTerminal = terminalComponent.createObject(tiledTerminalView,
+                                                         {"initialWorkingDirectory": initialWorkingDirectory});
+        tiledTerminalView.setOrientation(terminal, orientation);
+        tiledTerminalView.add(terminal, newTerminal, Qt.AlignTrailing);
+    }
+
+    handleDelegate: Rectangle {
+        implicitWidth: units.dp(1)
+        implicitHeight: units.dp(1)
+        color: focusedTerminal ? focusedTerminal.contourColor : ""
+    }
+
+    Component.onCompleted: {
+        var newTerminal = terminalComponent.createObject(tiledTerminalView,
+                                                         {"initialWorkingDirectory": initialWorkingDirectory});
+        setRootItem(newTerminal);
+    }
+
+    function moveFocus(direction) {
+        var terminal = tiledTerminalView.closestTileInDirection(focusedTerminal, direction);
+        if (terminal) {
+            terminal.focus = true;
+        }
+    }
+
+    Shortcut {
+        sequence: settings.shortcutMoveToTileRight
+        enabled: tiledTerminalView.focus
+        onActivated: moveFocus(Qt.AlignRight)
+    }
+
+    Shortcut {
+        sequence: settings.shortcutMoveToTileLeft
+        enabled: tiledTerminalView.focus
+        onActivated: moveFocus(Qt.AlignLeft)
+    }
+
+    Shortcut {
+        sequence: settings.shortcutMoveToTileAbove
+        enabled: tiledTerminalView.focus
+        onActivated: moveFocus(Qt.AlignTop)
+    }
+
+    Shortcut {
+        sequence: settings.shortcutMoveToTileBelow
+        enabled: tiledTerminalView.focus
+        onActivated: moveFocus(Qt.AlignBottom)
+    }
+
+    property real minimumTileWidth: units.gu(10)
+    property real minimumTileHeight: units.gu(10)
+    property Component terminalComponent: Terminal {
+        id: terminal
+        Component.onCompleted: if (focus) tiledTerminalView.focusedTerminal = terminal
+        onFocusChanged: if (focus) tiledTerminalView.focusedTerminal = terminal
+        onFinished: {
+            if (terminal.focus) {
+                var nextTerminal = tiledTerminalView.closestTile(terminal);
+                if (nextTerminal) nextTerminal.focus = true;
+            }
+            tiledTerminalView.remove(terminal);
+            terminal.destroy();
+        }
+    }
+}

=== added file 'src/app/qml/TiledView.qml'
--- src/app/qml/TiledView.qml	1970-01-01 00:00:00 +0000
+++ src/app/qml/TiledView.qml	2017-01-12 13:26:17 +0000
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2016-2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored-by: Florian Boucault <florian.boucault@xxxxxxxxxxxxx>
+ */
+import QtQuick 2.5
+import "binarytree.js" as BinaryTree
+
+FocusScope {
+    id: tiledView
+
+    property Component handleDelegate: Rectangle {
+        implicitWidth: units.dp(1)
+        implicitHeight: units.dp(1)
+        color: "white"
+    }
+
+    property int count: 0
+
+    // FIXME: odd semantics: what if setRootItem is called later?
+    function setRootItem(rootItem) {
+        if (rootItem && rootItem === __rootNode.value) {
+            return null;
+        }
+
+        var oldRoot = __rootNode.value;
+        if (rootItem) {
+            count = 1;
+        } else {
+            count = 0;
+            __rootNode.cleanup();
+        }
+        __rootNode.setValue(rootItem);
+
+        return oldRoot;
+    }
+
+    property var __rootNode: new BinaryTree.Node()
+    Component.onDestruction: __rootNode.cleanup()
+
+    Component.onCompleted: {
+        __rootNode.setWidth(width);
+        __rootNode.setHeight(height);
+    }
+
+    onWidthChanged: __rootNode.setWidth(width)
+    onHeightChanged: __rootNode.setHeight(height)
+
+    Component {
+        id: separatorComponent
+        TiledViewSeparator {
+            handleDelegate: tiledView.handleDelegate
+        }
+    }
+
+    function add(obj, newObj, side) {
+        var node = __rootNode.findNodeWithValue(obj);
+        var otherSide;
+        if (side == Qt.AlignLeading) {
+            otherSide = Qt.AlignTrailing;
+        } else {
+            otherSide = Qt.AlignLeading;
+        }
+
+        node.value = null;
+        node.setLeftRatio(0.5);
+        var separator = separatorComponent.createObject(tiledView, {"node": node});
+        node.setSeparator(separator);
+
+        var nodeSide = new BinaryTree.Node();
+        nodeSide.setValue(newObj);
+        node.setChild(side, nodeSide);
+
+        var nodeOtherSide = new BinaryTree.Node();
+        nodeOtherSide.setValue(obj);
+        node.setChild(otherSide, nodeOtherSide);
+        count += 1;
+    }
+
+    function remove(obj) {
+        var node = __rootNode.findNodeWithValue(obj);
+        var sibling = node.getSibling();
+        if (sibling) {
+            node.parent.copy(sibling);
+        }
+        count -= 1;
+    }
+
+    function closestTile(obj) {
+        var node = __rootNode.findNodeWithValue(obj);
+        var sibling = node.closestNodeWithValue();
+        if (sibling) {
+            return sibling.value;
+        } else {
+            return null;
+        }
+    }
+
+    function closestTileInDirection(obj, direction) {
+        var node = __rootNode.findNodeWithValue(obj);
+        var closestNode = node.closestNodeWithValueInDirection(direction);
+        if (closestNode && closestNode.value) {
+            return closestNode.value;
+        } else {
+            return null;
+        }
+    }
+
+    function getOrientation(obj) {
+        var node = __rootNode.findNodeWithValue(obj);
+        return node.orientation;
+    }
+
+    function setOrientation(obj, orientation) {
+        var node = __rootNode.findNodeWithValue(obj);
+        node.setOrientation(orientation);
+    }
+
+    function move(obj, targetObj, side) {
+        remove(obj);
+        add(targetObj, obj, side);
+    }
+}

=== added file 'src/app/qml/TiledViewSeparator.qml'
--- src/app/qml/TiledViewSeparator.qml	1970-01-01 00:00:00 +0000
+++ src/app/qml/TiledViewSeparator.qml	2017-01-12 13:26:17 +0000
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016-2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored-by: Florian Boucault <florian.boucault@xxxxxxxxxxxxx>
+ */
+import QtQuick 2.4
+import Ubuntu.Components 1.3
+
+Item {
+    id: separator
+
+    property int orientation: Qt.Horizontal
+    property var node
+    property Component handleDelegate
+
+    Loader {
+        id: handleLoader
+        sourceComponent: handleDelegate
+        anchors.fill: parent
+    }
+
+    implicitWidth: handleLoader.implicitWidth
+    implicitHeight: handleLoader.implicitHeight
+    z: 1
+
+    MouseArea {
+        anchors.centerIn: parent
+        width: orientation == Qt.Vertical ? units.gu(1) : parent.width
+        height: orientation == Qt.Vertical ? parent.height : units.gu(1)
+        cursorShape: orientation == Qt.Horizontal ? Qt.SizeVerCursor : Qt.SizeHorCursor
+        drag {
+            axis: orientation == Qt.Horizontal ? Drag.YAxis : Drag.XAxis
+            target: resizer
+            smoothed: false
+        }
+        onPressed: {
+            resizer.initialRatio = node.leftRatio;
+            resizer.x = 0;
+            resizer.y = 0;
+        }
+        enabled: separator.visible
+    }
+
+    function clamp(value, min, max) {
+        return Math.min(Math.max(min, value), max);
+    }
+
+    Item {
+        id: resizer
+        property real initialRatio
+        property real minimumRatio: 0.1
+        parent: null
+        onXChanged: {
+            var ratio = initialRatio + x / node.width;
+            ratio = clamp(ratio, minimumRatio, 1.0-minimumRatio);
+            node.setLeftRatio(ratio);
+        }
+        onYChanged: {
+            var ratio = initialRatio + y / node.height;
+            ratio = clamp(ratio, minimumRatio, 1.0-minimumRatio);
+            node.setLeftRatio(ratio);
+        }
+    }
+}

=== added file 'src/app/qml/binarytree.js'
--- src/app/qml/binarytree.js	1970-01-01 00:00:00 +0000
+++ src/app/qml/binarytree.js	2017-01-12 13:26:17 +0000
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2016-2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored-by: Florian Boucault <florian.boucault@xxxxxxxxxxxxx>
+ */
+.pragma library
+
+function Node() {
+    this.value = null;
+    this.parent = null;
+    this.left = null;
+    this.right = null;
+    this.separator = null;
+    this.leftRatio = 0.5;
+    this.rightRatio = 0.5;
+    this.orientation = Qt.Horizontal;
+    this.originX = 0;
+    this.originY = 0;
+    this.x = 0;
+    this.y = 0;
+    this.width = 0;
+    this.height = 0;
+}
+
+Node.prototype.cleanup = function cleanup(value) {
+    if (this.separator) {
+        this.separator.destroy();
+        this.separator = null;
+    }
+    this.parent = null;
+    this.value = null;
+    if (this.left) {
+        this.left.cleanup();
+        this.left = null;
+    }
+    if (this.right) {
+        this.right.cleanup();
+        this.right = null;
+    }
+}
+
+Node.prototype.setValue = function setValue(value) {
+    this.value = value;
+    this.updateX();
+    this.updateY();
+    this.updateWidth();
+    this.updateHeight();
+}
+
+Node.prototype.updateWidth = function updateWidth() {
+    if (this.value) {
+        this.value.width = this.width;
+    } else if (this.orientation == Qt.Horizontal) {
+        if (this.right && !this.left) {
+            this.right.setWidth(this.width);
+            this.right.setX(0);
+        }
+        if (this.left && !this.right) {
+            this.left.setWidth(this.width);
+            this.left.setX(0);
+        }
+        if (this.right && this.left) {
+            this.left.setWidth(this.width * this.leftRatio);
+            this.left.setX(0);
+            this.right.setWidth(this.width * this.rightRatio);
+            this.right.setX(this.left.width);
+        }
+    } else if (this.orientation == Qt.Vertical) {
+        if (this.left) {
+            this.left.setWidth(this.width);
+            this.left.setX(0);
+        }
+        if (this.right) {
+            this.right.setWidth(this.width);
+            this.right.setX(0);
+        }
+    }
+    this.updateSeparator();
+}
+
+Node.prototype.setWidth = function setWidth(width) {
+    this.width = width;
+    this.updateWidth();
+};
+
+Node.prototype.updateHeight = function updateHeight() {
+    if (this.value) {
+        this.value.height = this.height;
+    } else if (this.orientation == Qt.Vertical) {
+        if (this.right && !this.left) {
+            this.right.setHeight(this.height);
+            this.right.setY(0);
+        }
+        if (this.left && !this.right) {
+            this.left.setHeight(this.height);
+            this.left.setY(0);
+        }
+        if (this.right && this.left) {
+            this.left.setHeight(this.height * this.leftRatio);
+            this.left.setY(0);
+            this.right.setHeight(this.height * this.rightRatio);
+            this.right.setY(this.left.height);
+        }
+    } else if (this.orientation == Qt.Horizontal) {
+        if (this.left) {
+            this.left.setHeight(this.height);
+            this.left.setY(0);
+        }
+        if (this.right) {
+            this.right.setHeight(this.height);
+            this.right.setY(0);
+        }
+    }
+    this.updateSeparator();
+}
+
+Node.prototype.setHeight = function setHeight(height) {
+    this.height = height;
+    this.updateHeight();
+};
+
+Node.prototype.setLeftRatio = function setLeftRatio(ratio) {
+    this.leftRatio = ratio;
+    this.rightRatio = 1.0 - this.leftRatio;
+    if (this.orientation == Qt.Horizontal) {
+        this.updateWidth();
+    } else {
+        this.updateHeight();
+    }
+};
+
+Node.prototype.setRightRatio = function setRightRatio(ratio) {
+    this.rightRatio = ratio;
+    this.leftRatio = 1.0 - this.rightRatio;
+    if (this.orientation == Qt.Horizontal) {
+        this.updateWidth();
+    } else {
+        this.updateHeight();
+    }
+};
+
+Node.prototype.setChild = function setChild(side, childNode) {
+    switch (side) {
+        case Qt.AlignLeading:
+            if (this.left) {
+                // FIXME: breaks copy()
+//                this.left.cleanup();
+            }
+            this.left = childNode;
+            break;
+        case Qt.AlignTrailing:
+            if (this.right) {
+                // FIXME: breaks copy()
+//                this.right.cleanup();
+            }
+            this.right = childNode;
+            break;
+        default:
+            break;
+    }
+    if (childNode) {
+        childNode.parent = this;
+    }
+    this.updateX();
+    this.updateY();
+    this.updateWidth();
+    this.updateHeight();
+}
+
+Node.prototype.updateSeparator = function updateSeparator() {
+    if (this.separator) {
+        this.separator.orientation = this.orientation == Qt.Vertical ? Qt.Horizontal : Qt.Vertical;
+        if (this.left && this.right) {
+            // FIXME: separator should be centered
+            this.separator.x = this.right.originX + this.right.x;
+            this.separator.y = this.right.originY + this.right.y;
+            if (this.separator.orientation == Qt.Vertical) {
+                this.separator.width = this.separator.implicitWidth;
+                this.separator.height = this.right.height;
+            } else if (this.separator.orientation == Qt.Horizontal) {
+                this.separator.width = this.right.width;
+                this.separator.height = this.separator.implicitHeight;
+            }
+            this.separator.visible = true;
+        } else {
+            this.separator.visible = false;
+        }
+    }
+}
+
+Node.prototype.setSeparator = function setSeparator(separator) {
+    this.separator = separator;
+    this.updateSeparator();
+}
+
+Node.prototype.copy = function copy(otherNode) {
+    if (!otherNode) return;
+    var newParent = otherNode.parent;
+
+    this.orientation = otherNode.orientation;
+    this.setValue(otherNode.value);
+    this.setChild(Qt.AlignLeading, otherNode.left);
+    this.setChild(Qt.AlignTrailing, otherNode.right);
+
+    if (newParent.left === otherNode) {
+        newParent.setChild(Qt.AlignLeading, this);
+    } else if (otherNode.parent.right === otherNode) {
+        newParent.setChild(Qt.AlignTrailing, this);
+    }
+    otherNode.left = null;
+    otherNode.right = null;
+    otherNode.cleanup();
+    this.updateX();
+    this.updateY();
+    this.updateWidth();
+    this.updateHeight();
+}
+
+Node.prototype.setOrientation = function setOrientation(orientation) {
+    this.orientation = orientation;
+    this.updateWidth();
+    this.updateHeight();
+    this.updateSeparator();
+}
+
+Node.prototype.updateX = function updateX() {
+    var absoluteX = this.originX + this.x;
+    if (this.value) {
+        this.value.x = Math.round(absoluteX);
+    } else {
+        if (this.left) {
+            this.left.setOriginX(absoluteX);
+        }
+        if (this.right) {
+            this.right.setOriginX(absoluteX);
+        }
+    }
+    this.updateSeparator();
+}
+
+Node.prototype.setX = function setX(x) {
+    this.x = x;
+    this.updateX();
+};
+
+Node.prototype.setOriginX = function setOriginX(x) {
+    this.originX = x;
+    this.updateX();
+};
+
+Node.prototype.updateY = function updateY() {
+    var absoluteY = this.originY + this.y;
+    if (this.value) {
+        this.value.y = Math.round(absoluteY);
+    } else {
+        if (this.left) {
+            this.left.setOriginY(absoluteY);
+        }
+        if (this.right) {
+            this.right.setOriginY(absoluteY);
+        }
+    }
+    this.updateSeparator();
+}
+
+Node.prototype.setY = function setY(y) {
+    this.y = y;
+    this.updateY();
+};
+
+Node.prototype.setOriginY = function setOriginY(y) {
+    this.originY = y;
+    this.updateY();
+};
+
+Node.prototype.getSibling = function getSibling() {
+    if (this.parent) {
+        if (this.parent.left === this) {
+            return this.parent.right;
+        } else {
+            return this.parent.left;
+        }
+    }
+    return null;
+}
+
+Node.prototype.findNodeWithValue = function findNodeWithValue(value) {
+    if (this.value === value) return this;
+    var result;
+    if (this.left) {
+        result = this.left.findNodeWithValue(value);
+        if (result) {
+            return result;
+        }
+    }
+    if (this.right) {
+        result = this.right.findNodeWithValue(value);
+        if (result) {
+            return result;
+        }
+    }
+    return null;
+};
+
+Node.prototype.closestChildWithValue = function closestChildWithValue(sides) {
+    var currentLevelNodes = [this];
+    var nextLevelNodes = [];
+    while (currentLevelNodes.length != 0) {
+        for (var i=0; i<currentLevelNodes.length; i++) {
+            var node = currentLevelNodes[i];
+            if (node.value) {
+                return node;
+            }
+            if ((sides == undefined || sides & Qt.AlignLeading) && node.left) {
+                nextLevelNodes.push(node.left);
+            }
+            if ((sides == undefined || sides & Qt.AlignTrailing) && node.right) {
+                nextLevelNodes.push(node.right);
+            }
+        }
+        currentLevelNodes = nextLevelNodes;
+        nextLevelNodes = [];
+    }
+    return null;
+}
+
+Node.prototype.closestNodeWithValue = function closestNodeWithValue() {
+    // explore sibling hierarchy
+    var sibling = this.getSibling();
+    if (sibling) {
+        var closestChild = sibling.closestChildWithValue(Qt.AlignLeading | Qt.AlignTrailing);
+        if (closestChild) {
+            return closestChild;
+        }
+    }
+    // explore parent's sibling hierarchy
+    if (this.parent) {
+        var parentSibling = this.parent.getSibling();
+        if (parentSibling) {
+            var closestChild = parentSibling.closestChildWithValue(Qt.AlignLeading | Qt.AlignTrailing);
+            if (closestChild) {
+                return closestChild;
+            }
+        }
+    }
+    return null;
+};
+
+Node.prototype.closestNodeWithValueInDirection = function closestNodeWithValueInDirection(direction) {
+    if (this.parent) {
+        if (this.parent.left === this) {
+            if ((direction == Qt.AlignRight && this.parent.orientation == Qt.Horizontal) ||
+                (direction == Qt.AlignBottom && this.parent.orientation == Qt.Vertical)) {
+                return this.parent.right.closestChildWithValue(Qt.AlignLeading);
+            }
+        } else if (this.parent.right === this) {
+            if ((direction == Qt.AlignLeft && this.parent.orientation == Qt.Horizontal) ||
+                (direction == Qt.AlignTop && this.parent.orientation == Qt.Vertical)) {
+                return this.parent.left.closestChildWithValue(Qt.AlignTrailing);
+            }
+        }
+        return this.parent.closestNodeWithValueInDirection(direction);
+    } else {
+        return null;
+    }
+}

=== modified file 'src/plugin/qmltermwidget/lib/TerminalDisplay.h'
--- src/plugin/qmltermwidget/lib/TerminalDisplay.h	2017-01-06 08:37:18 +0000
+++ src/plugin/qmltermwidget/lib/TerminalDisplay.h	2017-01-12 13:26:17 +0000
@@ -898,8 +898,8 @@
 
     //the delay in milliseconds between redrawing blinking text
     static const int TEXT_BLINK_DELAY = 500;
-    static const int DEFAULT_LEFT_MARGIN = 1;
-    static const int DEFAULT_TOP_MARGIN = 1;
+    static const int DEFAULT_LEFT_MARGIN = 8;
+    static const int DEFAULT_TOP_MARGIN = 8;
 
     // QMLTermWidget port functions
     QFont m_font;

=== modified file 'tests/CMakeLists.txt'
--- tests/CMakeLists.txt	2015-02-24 23:23:36 +0000
+++ tests/CMakeLists.txt	2017-01-12 13:26:17 +0000
@@ -1,1 +1,2 @@
 add_subdirectory(autopilot)
+add_subdirectory(qtquicktest)

=== added directory 'tests/qtquicktest'
=== added file 'tests/qtquicktest/CMakeLists.txt'
--- tests/qtquicktest/CMakeLists.txt	1970-01-01 00:00:00 +0000
+++ tests/qtquicktest/CMakeLists.txt	2017-01-12 13:26:17 +0000
@@ -0,0 +1,20 @@
+find_program(XVFB_RUN_BIN xvfb-run)
+if(NOT XVFB_RUN_BIN)
+  message(FATAL_ERROR "Could not find xvfb-run, please install the xvfb package")
+endif()
+set(XVFB_RUN_CMD ${XVFB_RUN_BIN} -a -s "-screen 0 1024x768x24")
+
+include(FindPkgConfig)
+find_package(Qt5Core)
+
+# copy qml test files to build directory
+if(NOT "${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_CURRENT_BINARY_DIR}")
+add_custom_target(qmlTestFiles ALL
+    COMMAND cp ${CMAKE_CURRENT_SOURCE_DIR}/*.qml ${CMAKE_CURRENT_BINARY_DIR}
+)
+endif(NOT "${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_CURRENT_BINARY_DIR}")
+
+set(QTQUICK_TEST tst_qtquicktest)
+add_executable(${QTQUICK_TEST} qtquicktest.cpp)
+qt5_use_modules(${QTQUICK_TEST} Core Qml Quick Test QuickTest)
+add_test(${QTQUICK_TEST} ${XVFB_RUN_CMD} ${CMAKE_CURRENT_BINARY_DIR}/${QTQUICK_TEST})

=== added file 'tests/qtquicktest/qtquicktest.cpp'
--- tests/qtquicktest/qtquicktest.cpp	1970-01-01 00:00:00 +0000
+++ tests/qtquicktest/qtquicktest.cpp	2017-01-12 13:26:17 +0000
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2012 Canonical, Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QtQuickTest/quicktest.h>
+QUICK_TEST_MAIN(qtquicktest)

=== added file 'tests/qtquicktest/tst_TiledView.qml'
--- tests/qtquicktest/tst_TiledView.qml	1970-01-01 00:00:00 +0000
+++ tests/qtquicktest/tst_TiledView.qml	2017-01-12 13:26:17 +0000
@@ -0,0 +1,417 @@
+/*
+ * Copyright (C) 2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored-by: Florian Boucault <florian.boucault@xxxxxxxxxxxxx>
+ */
+
+import QtQuick 2.4
+import QtTest 1.0
+import "../../src/app/qml"
+
+TestCase {
+    name: "TiledView"
+
+    TiledView {
+        id: tiledView
+        width: 800
+        height: 600
+    }
+
+    property Component objectComponent: Rectangle {
+        width: 100
+        height: 50
+        color: "red"
+    }
+
+    SignalSpy {
+        id: countSpy
+        target: tiledView
+        signalName: "countChanged"
+    }
+
+    function init() {
+        countSpy.clear();
+    }
+
+    function test_defaults() {
+        compare(tiledView.count, 0);
+    }
+
+    function test_setRootItemValid() {
+        var newObject = objectComponent.createObject(tiledView);
+        var oldRoot = tiledView.setRootItem(newObject);
+        compare(oldRoot, null);
+        compare(countSpy.count, 1);
+        compare(tiledView.count, 1);
+        compare(newObject.width, tiledView.width);
+        compare(newObject.height, tiledView.height);
+        compare(tiledView.getOrientation(newObject), Qt.Horizontal);
+        tiledView.setRootItem(null);
+        newObject.destroy();
+    }
+
+    function test_setRootItemNull() {
+        var oldRoot = tiledView.setRootItem(null);
+        compare(oldRoot, null);
+        compare(countSpy.count, 0);
+        compare(tiledView.count, 0);
+    }
+
+    function test_resetRootItem() {
+        // set to new object
+        var newObject = objectComponent.createObject(tiledView);
+        var oldRoot = tiledView.setRootItem(newObject);
+        compare(oldRoot, null);
+        compare(countSpy.count, 1);
+        compare(tiledView.count, 1);
+
+        // set to same object
+        oldRoot = tiledView.setRootItem(newObject);
+        compare(oldRoot, null);
+        compare(countSpy.count, 1);
+        compare(tiledView.count, 1);
+
+        // set to another new object
+        var newObject2 = objectComponent.createObject(tiledView);
+        oldRoot = tiledView.setRootItem(newObject2);
+        compare(oldRoot, newObject);
+        compare(countSpy.count, 1);
+        compare(tiledView.count, 1);
+
+        // set to null
+        oldRoot = tiledView.setRootItem(null);
+        compare(oldRoot, newObject2);
+        compare(countSpy.count, 2);
+        compare(tiledView.count, 0);
+
+        newObject.destroy();
+        newObject2.destroy();
+    }
+
+    function verifySetRootItem(object) {
+        tiledView.setRootItem(object);
+        compare(tiledView.count, 1);
+        compare(object.width, tiledView.width);
+        compare(object.height, tiledView.height);
+    }
+
+    function verifySetOrientation(object, orientation) {
+        tiledView.setOrientation(object, orientation);
+        compare(tiledView.getOrientation(object), orientation);
+    }
+
+    function verifyAdd(object, newObject, side) {
+        var previousX = object.x;
+        var previousY = object.y;
+        var previousWidth = object.width;
+        var previousHeight = object.height;
+        var previousCount = tiledView.count;
+        var orientation = tiledView.getOrientation(object);
+
+        tiledView.add(object, newObject, side);
+        compare(tiledView.count, previousCount+1);
+        compare(tiledView.getOrientation(object), Qt.Horizontal);
+        compare(tiledView.getOrientation(newObject), Qt.Horizontal);
+
+        if (orientation == Qt.Horizontal) {
+            compare(object.width, previousWidth / 2);
+            compare(object.height, previousHeight);
+            compare(newObject.width, previousWidth / 2);
+            compare(newObject.height, previousHeight);
+            if (side == Qt.AlignTrailing) {
+                compare(object.x, previousX);
+                compare(object.y, previousY);
+                compare(newObject.x, previousX + Math.round(previousWidth / 2));
+                compare(newObject.y, previousY);
+            } else if (side == Qt.AlignLeading) {
+                compare(newObject.x, previousX);
+                compare(newObject.y, previousY);
+                compare(object.x, previousX + Math.round(previousWidth / 2));
+                compare(object.y, previousY);
+            }
+        } else if (orientation == Qt.Vertical) {
+            compare(object.width, previousWidth);
+            compare(object.height, previousHeight / 2);
+            compare(newObject.width, previousWidth);
+            compare(newObject.height, previousHeight / 2);
+            if (side == Qt.AlignTrailing) {
+                compare(object.x, previousX);
+                compare(object.y, previousY);
+                compare(newObject.x, previousX);
+                compare(newObject.y, previousY + Math.round(previousHeight / 2));
+            } else if (side == Qt.AlignLeading) {
+                compare(newObject.x, previousX);
+                compare(newObject.y, previousY);
+                compare(object.x, previousX);
+                compare(object.y, previousY + Math.round(previousHeight / 2));
+            }
+        }
+    }
+
+    function verifyRemove(object) {
+        var node = tiledView.__rootNode.findNodeWithValue(object);
+        var siblingNode = node.getSibling();
+        var siblingObject = siblingNode.value;
+
+        var side;
+        if (node.parent.left === node) {
+            side = Qt.AlignLeading;
+        } else {
+            side = Qt.AlignTrailing;
+        }
+        var orientation = node.parent.orientation;
+
+        var expectedX;
+        var expectedY;
+        var expectedWidth;
+        var expectedHeight;
+        if (orientation == Qt.Horizontal) {
+            expectedWidth = object.width + siblingNode.width;
+            expectedHeight = siblingNode.height;
+            if (side == Qt.AlignTrailing) {
+                expectedX = siblingNode.x;
+                expectedY = siblingNode.y;
+            } else if (side == Qt.AlignLeading) {
+                expectedX = object.x;
+                expectedY = siblingNode.y;
+            }
+        } else if (orientation == Qt.Vertical) {
+            expectedWidth = siblingNode.width;
+            expectedHeight = object.height + siblingNode.height;
+            if (side == Qt.AlignTrailing) {
+                expectedX = siblingNode.x;
+                expectedY = siblingNode.y;
+            } else if (side == Qt.AlignLeading) {
+                expectedX = siblingNode.x;
+                expectedY = object.y;
+            }
+        }
+
+        var previousCount = tiledView.count;
+
+        tiledView.remove(object);
+        // TODO: we verify that the resulting node has the correct x/y,width/height
+        // but we could go further and verify that all its children also do
+        var removeNode = tiledView.__rootNode.findNodeWithValue(siblingObject);
+        compare(tiledView.count, previousCount-1);
+        compare(removeNode.width, expectedWidth);
+        compare(removeNode.height, expectedHeight);
+        compare(removeNode.x, expectedX);
+        compare(removeNode.y, expectedY);
+    }
+
+    function test_simpleAdd_data() {
+        return [
+                    {orientation: Qt.Horizontal, side: Qt.AlignTrailing},
+                    {orientation: Qt.Vertical, side: Qt.AlignTrailing},
+                    {orientation: Qt.Horizontal, side: Qt.AlignLeading},
+                    {orientation: Qt.Vertical, side: Qt.AlignLeading},
+        ];
+    }
+
+    function test_simpleAdd(data) {
+        var rootObject = objectComponent.createObject(tiledView);
+        verifySetRootItem(rootObject);
+        verifySetOrientation(rootObject, data.orientation);
+
+        var newObject = objectComponent.createObject(tiledView);
+        verifyAdd(rootObject, newObject, data.side);
+
+        tiledView.setRootItem(null);
+        rootObject.destroy();
+        newObject.destroy();
+    }
+
+    function test_nestedAdds() {
+        var objects = [];
+
+        objects["0"] = objectComponent.createObject(tiledView);
+        verifySetRootItem(objects["0"]);
+
+        objects["1"] = objectComponent.createObject(tiledView);
+        verifySetOrientation(objects["0"], Qt.Horizontal);
+        verifyAdd(objects["0"], objects["1"], Qt.AlignTrailing);
+
+        objects["2"] = objectComponent.createObject(tiledView);
+        verifySetOrientation(objects["1"], Qt.Horizontal);
+        verifyAdd(objects["1"], objects["2"], Qt.AlignTrailing);
+
+        objects["3"] = objectComponent.createObject(tiledView);
+        verifySetOrientation(objects["2"], Qt.Horizontal);
+        verifyAdd(objects["2"], objects["3"], Qt.AlignTrailing);
+
+        objects["4"] = objectComponent.createObject(tiledView);
+        verifySetOrientation(objects["3"], Qt.Horizontal);
+        verifyAdd(objects["3"], objects["4"], Qt.AlignTrailing);
+
+        objects["5"] = objectComponent.createObject(tiledView);
+        verifySetOrientation(objects["4"], Qt.Horizontal);
+        verifyAdd(objects["4"], objects["5"], Qt.AlignTrailing);
+
+        objects["6"] = objectComponent.createObject(tiledView);
+        verifySetOrientation(objects["5"], Qt.Horizontal);
+        verifyAdd(objects["5"], objects["6"], Qt.AlignTrailing);
+
+        tiledView.setRootItem(null);
+        for (var i=0; i<objects.length; i++) {
+            objects[i].destroy();
+        }
+    }
+
+    function test_resizeView() {
+        var leftObject = objectComponent.createObject(tiledView);
+        verifySetRootItem(leftObject);
+
+        var bottomRightObject = objectComponent.createObject(tiledView);
+        verifySetOrientation(leftObject, Qt.Horizontal);
+        verifyAdd(leftObject, bottomRightObject, Qt.AlignTrailing);
+
+        var topRightObject = objectComponent.createObject(tiledView);
+        verifySetOrientation(bottomRightObject, Qt.Vertical);
+        verifyAdd(bottomRightObject, topRightObject, Qt.AlignLeading);
+
+        var initialWidth = tiledView.width;
+        var initialHeight = tiledView.height;
+        var factor = 0.7;
+
+        // storing sizes before resizing
+        var sizes = {"leftObject": {"width": leftObject.width, "height": leftObject.height},
+                     "bottomRightObject": {"width": bottomRightObject.width, "height": bottomRightObject.height},
+                     "topRightObject": {"width": topRightObject.width, "height": topRightObject.height}};
+        tiledView.width = initialWidth * factor;
+        compare(leftObject.width, sizes.leftObject.width * factor);
+        compare(bottomRightObject.width, sizes.bottomRightObject.width * factor);
+        compare(topRightObject.width, sizes.topRightObject.width * factor);
+        compare(leftObject.height, sizes.leftObject.height);
+        compare(bottomRightObject.height, sizes.bottomRightObject.height);
+        compare(topRightObject.height, sizes.topRightObject.height);
+
+        tiledView.height = initialHeight * factor;
+        compare(leftObject.width, sizes.leftObject.width * factor);
+        compare(bottomRightObject.width, sizes.bottomRightObject.width * factor);
+        compare(topRightObject.width, sizes.topRightObject.width * factor);
+        compare(leftObject.height, sizes.leftObject.height * factor);
+        compare(bottomRightObject.height, sizes.bottomRightObject.height * factor);
+        compare(topRightObject.height, sizes.topRightObject.height * factor);
+
+        // resetting initial size
+        tiledView.width = initialWidth;
+        tiledView.height = initialHeight;
+        compare(leftObject.width, sizes.leftObject.width);
+        compare(bottomRightObject.width, sizes.bottomRightObject.width);
+        compare(topRightObject.width, sizes.topRightObject.width);
+        compare(leftObject.height, sizes.leftObject.height);
+        compare(bottomRightObject.height, sizes.bottomRightObject.height);
+        compare(topRightObject.height, sizes.topRightObject.height);
+
+        tiledView.setRootItem(null);
+        leftObject.destroy();
+        bottomRightObject.destroy();
+        topRightObject.destroy();
+    }
+
+    function test_simpleRemove_data() {
+        return [
+                    {orientation: Qt.Horizontal, side: Qt.AlignTrailing},
+                    {orientation: Qt.Vertical, side: Qt.AlignTrailing},
+                    {orientation: Qt.Horizontal, side: Qt.AlignLeading},
+                    {orientation: Qt.Vertical, side: Qt.AlignLeading},
+        ];
+    }
+
+    function test_simpleRemove(data) {
+        var rootObject = objectComponent.createObject(tiledView);
+        verifySetRootItem(rootObject);
+        verifySetOrientation(rootObject, data.orientation);
+
+        var newObject = objectComponent.createObject(tiledView);
+        verifyAdd(rootObject, newObject, data.side);
+        verifyRemove(newObject);
+
+        verifyAdd(rootObject, newObject, data.side);
+        verifyRemove(rootObject);
+
+        verifyAdd(newObject, rootObject, data.side);
+        verifyRemove(newObject);
+
+        tiledView.setRootItem(null);
+        rootObject.destroy();
+        newObject.destroy();
+    }
+
+    function test_nestedAddsRemoveRoot() {
+        var objects = [];
+
+        objects["0"] = objectComponent.createObject(tiledView);
+        verifySetRootItem(objects["0"]);
+
+        objects["1"] = objectComponent.createObject(tiledView);
+        verifySetOrientation(objects["0"], Qt.Horizontal);
+        verifyAdd(objects["0"], objects["1"], Qt.AlignTrailing);
+
+        objects["2"] = objectComponent.createObject(tiledView);
+        verifySetOrientation(objects["1"], Qt.Horizontal);
+        verifyAdd(objects["1"], objects["2"], Qt.AlignTrailing);
+
+        // remove root
+        verifyRemove(objects["0"]);
+
+        // add further
+        objects["3"] = objectComponent.createObject(tiledView);
+        verifySetOrientation(objects["2"], Qt.Horizontal);
+        verifyAdd(objects["2"], objects["3"], Qt.AlignTrailing);
+
+        verifyRemove(objects["2"]);
+
+        objects["4"] = objectComponent.createObject(tiledView);
+        verifySetOrientation(objects["1"], Qt.Horizontal);
+        verifyAdd(objects["1"], objects["4"], Qt.AlignTrailing);
+
+        verifyRemove(objects["1"]);
+
+        tiledView.setRootItem(null);
+        for (var i=0; i<objects.length; i++) {
+            objects[i].destroy();
+        }
+    }
+
+    function test_AddsVertical() {
+        var objects = [];
+
+        objects["0"] = objectComponent.createObject(tiledView);
+        verifySetRootItem(objects["0"]);
+
+        objects["1"] = objectComponent.createObject(tiledView);
+        verifySetOrientation(objects["0"], Qt.Vertical);
+        verifyAdd(objects["0"], objects["1"], Qt.AlignTrailing);
+
+        objects["2"] = objectComponent.createObject(tiledView);
+        verifySetOrientation(objects["1"], Qt.Vertical);
+        verifyAdd(objects["1"], objects["2"], Qt.AlignTrailing);
+
+        objects["3"] = objectComponent.createObject(tiledView);
+        verifySetOrientation(objects["2"], Qt.Vertical);
+        verifyAdd(objects["2"], objects["3"], Qt.AlignLeading);
+
+        objects["4"] = objectComponent.createObject(tiledView);
+        verifySetOrientation(objects["3"], Qt.Vertical);
+        verifyAdd(objects["3"], objects["4"], Qt.AlignTrailing);
+
+        tiledView.setRootItem(null);
+        for (var i=0; i<objects.length; i++) {
+            objects[i].destroy();
+        }
+    }
+}


Follow ups