diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f0b4a0529a49cf97eaf13f655b5aaa47e4aabeff
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,78 @@
+# Use latest Ubuntu LTS docker image.
+image: ubuntu:xenial
+
+build:
+  stage: build
+
+  before_script:
+    - apt update
+    # Build dependencies
+    - apt -y install g++ cmake make git
+    - apt -y install qtbase5-private-dev qtscript5-dev qttools5-dev qttools5-dev-tools libqt5svg5-dev
+    # Optional: Better support for X11
+    - apt -y install libxfixes-dev libxtst-dev
+    # Optional: CMake can get version from git
+    - apt -y install git
+
+  script:
+    - mkdir -p build
+    - cd build
+    - cmake -DWITH_TESTS=TRUE -DWITH_QT5=TRUE -DCMAKE_INSTALL_PREFIX=../copyq -DCOPYQ_ITEMSYNC_UPDATE_INTERVAL_MS=1000 ..
+    - make install
+
+  # Upload installed application.
+  artifacts:
+    paths:
+      - copyq
+
+  cache:
+    paths:
+      - build
+
+# Run simple tests (doesn't require GUI)
+test:
+  stage: test
+
+  before_script:
+    - apt update
+    # Runtime libraries
+    - apt -y install libqt5core5a libqt5gui5 libqt5network5 libqt5script5 libqt5widgets5 libx11-6 libxtst6 libqt5svg5 libqt5xml5 libqt5test5
+
+  script:
+    - copyq/bin/copyq help
+    - copyq/bin/copyq version
+
+  dependencies:
+    - build
+
+# GUI tests (requires X11)
+test_gui:
+  stage: test
+
+  before_script:
+    - apt update
+    # Runtime libraries
+    - apt -y install libqt5core5a libqt5gui5 libqt5network5 libqt5script5 libqt5widgets5 libx11-6 libxtst6 libqt5svg5 libqt5xml5 libqt5test5
+    # X11 and window manager
+    - apt -y install xvfb openbox
+    # Screenshot utility
+    - apt -y install scrot
+
+  script:
+    - export DISPLAY=':99.0'
+    - Xvfb :99 -screen 0 640x480x24 &
+    - sleep 5
+    - openbox &
+    - sleep 5
+    # Take screenshots in intervals.
+    - (mkdir -p screenshots && while true; do i=$((i+1)); f="screenshots/$i.png"; sleep 1 && echo "   --- $f ---" && scrot "$f" || break; done) &
+    - copyq/bin/copyq tests
+
+  # Upload screenshots on failure.
+  artifacts:
+    when: on_failure
+    paths:
+      - screenshots
+
+  dependencies:
+    - build
diff --git a/plugins/itemimage/itemimage.cpp b/plugins/itemimage/itemimage.cpp
index 6f5735791a78deb49876ad7d01f4bf4f8a618ca3..1766c2bebaa07a6429d6b2987793de5d9325dd6b 100644
--- a/plugins/itemimage/itemimage.cpp
+++ b/plugins/itemimage/itemimage.cpp
@@ -209,8 +209,10 @@ ItemWidget *ItemImageLoader::create(const QModelIndex &index, QWidget *parent, b
 
 QStringList ItemImageLoader::formatsToSave() const
 {
-    return QStringList("image/svg+xml") << QString("image/bmp") << QString("image/png")
-                                        << QString("image/jpeg") << QString("image/gif");
+    return QStringList()
+            << QString("image/svg+xml")
+            << QString("image/png")
+            << QString("image/gif");
 }
 
 QVariantMap ItemImageLoader::applySettings()
diff --git a/plugins/itemsync/CMakeLists.txt b/plugins/itemsync/CMakeLists.txt
index 5addef5c95292aa1d5f94b18e680a876e9157800..3f810b3020843b81bcbd3fbbf046c72c00abda89 100644
--- a/plugins/itemsync/CMakeLists.txt
+++ b/plugins/itemsync/CMakeLists.txt
@@ -1,3 +1,8 @@
+OPTION(COPYQ_ITEMSYNC_UPDATE_INTERVAL_MS "Interval in ms for updating items from files" -1)
+if (COPYQ_ITEMSYNC_UPDATE_INTERVAL_MS GREATER 0)
+    add_definitions( -DCOPYQ_ITEMSYNC_UPDATE_INTERVAL_MS=${COPYQ_ITEMSYNC_UPDATE_INTERVAL_MS}  )
+endif()
+
 set(copyq_plugin_itemsync_SOURCES
     ../../src/common/common.cpp
     ../../src/common/config.cpp
diff --git a/plugins/itemsync/itemsync.cpp b/plugins/itemsync/itemsync.cpp
index 4d5cc73b1f6a13985a70908ffe8484bba005a105..4ea0e0f054aabbafd17f6a13b1c4ed256df86ed4 100644
--- a/plugins/itemsync/itemsync.cpp
+++ b/plugins/itemsync/itemsync.cpp
@@ -767,6 +767,16 @@ public:
     {
         m_watcher.addPath(path);
 
+#ifdef COPYQ_ITEMSYNC_UPDATE_INTERVAL_MS
+        {
+            auto t = new QTimer(this);
+            t->setInterval(COPYQ_ITEMSYNC_UPDATE_INTERVAL_MS);
+            connect( t, SIGNAL(timeout()),
+                     SLOT(updateItems()) );
+            t->start();
+        }
+#endif
+
         m_updateTimer.setInterval(updateItemsIntervalMs);
         m_updateTimer.setSingleShot(true);
         connect( &m_updateTimer, SIGNAL(timeout()),
@@ -846,6 +856,8 @@ public slots:
      */
     void updateItems()
     {
+        m_updateTimer.stop();
+
         if ( m_model.isNull() )
             return;
 
diff --git a/src/common/common.cpp b/src/common/common.cpp
index bce5c3f0cce627d1318a3c8643389ed01c36e1f1..d2708015ee076b56f5785b72505dac5be37f499d 100644
--- a/src/common/common.cpp
+++ b/src/common/common.cpp
@@ -46,6 +46,11 @@
 #   include <QTextDocument> // Qt::escape()
 #endif
 
+// This is needed on X11 when retrieving lots of data from clipboard.
+#if QT_VERSION >= 0x050000 && defined(COPYQ_WS_X11)
+#   define PROCESS_EVENTS_BEFORE_CLIPBOARD_DATA
+#endif
+
 namespace {
 
 QString getImageFormatFromMime(const QString &mime)
@@ -149,7 +154,6 @@ int indexOfKeyHint(const QString &name)
     return -1;
 }
 
-
 QString escapeHtmlSpaces(const QString &str)
 {
     QString str2 = str;
@@ -158,6 +162,24 @@ QString escapeHtmlSpaces(const QString &str)
             .replace('\n', "<br />");
 }
 
+QByteArray getUtf8Data(const QMimeData &data, const QString &format)
+{
+    if (format == mimeText || format == mimeHtml)
+        return dataToText( data.data(format), format ).toUtf8();
+
+    if (format == mimeUriList) {
+        QByteArray bytes;
+        for ( const auto &url : data.urls() ) {
+            if ( !bytes.isEmpty() )
+                bytes += '\n';
+            bytes += url.toString().toUtf8();
+        }
+        return bytes;
+    }
+
+    return data.data(format);
+}
+
 } // namespace
 
 QString quoteString(const QString &str)
@@ -207,24 +229,6 @@ uint hash(const QVariantMap &data)
     return hash;
 }
 
-QByteArray getUtf8Data(const QMimeData &data, const QString &format)
-{
-    if (format == mimeText || format == mimeHtml)
-        return dataToText( data.data(format), format ).toUtf8();
-
-    if (format == mimeUriList) {
-        QByteArray bytes;
-        for ( const auto &url : data.urls() ) {
-            if ( !bytes.isEmpty() )
-                bytes += '\n';
-            bytes += url.toString().toUtf8();
-        }
-        return bytes;
-    }
-
-    return data.data(format);
-}
-
 QString getTextData(const QByteArray &bytes)
 {
     // QString::fromUtf8(bytes) ends string at first '\0'.
@@ -261,7 +265,19 @@ QVariantMap cloneData(const QMimeData &data, const QStringList &formats)
     QImage image;
     bool imageLoaded = false;
 
+#ifdef PROCESS_EVENTS_BEFORE_CLIPBOARD_DATA
+    const QPointer<const QMimeData> dataGuard(&data);
+#endif
+
     for (const auto &mime : formats) {
+#ifdef PROCESS_EVENTS_BEFORE_CLIPBOARD_DATA
+        QCoreApplication::processEvents();
+        if (dataGuard.isNull()) {
+            log("Clipboard data lost", LogWarning);
+            return newdata;
+        }
+#endif
+
         const QByteArray bytes = getUtf8Data(data, mime);
         if ( !bytes.isEmpty() ) {
             newdata.insert(mime, bytes);
diff --git a/src/common/common.h b/src/common/common.h
index 914d3113229a824ab7c0093df5f8a4bc3abe8a17..387f7d32e33d40787f2cf080006fc3028c6b34b8 100644
--- a/src/common/common.h
+++ b/src/common/common.h
@@ -66,8 +66,6 @@ const QMimeData *clipboardData(QClipboard::Mode mode = QClipboard::Clipboard);
 
 uint hash(const QVariantMap &data);
 
-QByteArray getUtf8Data(const QMimeData &data, const QString &format);
-
 QString getTextData(const QByteArray &data);
 
 /**
diff --git a/src/gui/configurationmanager.cpp b/src/gui/configurationmanager.cpp
index bfb5ebe94b2b2c361a1baf55fdd7dea94a8fcf68..c52465a1ba6f02d86b3b1ec53264dcf7e8d32aa6 100644
--- a/src/gui/configurationmanager.cpp
+++ b/src/gui/configurationmanager.cpp
@@ -482,7 +482,9 @@ void ConfigurationManager::apply()
     // Language changes after restart.
     const int newLocaleIndex = ui->comboBoxLanguage->currentIndex();
     const QString newLocaleName = ui->comboBoxLanguage->itemData(newLocaleIndex).toString();
-    const QString oldLocaleName = settings.value("Options/language").toString();
+    QString oldLocaleName = settings.value("Options/language").toString();
+    if (oldLocaleName.isEmpty())
+        oldLocaleName = "en";
     const QLocale oldLocale;
 
     settings.setValue("Options/language", newLocaleName);
diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp
index f95daf2d4689af6948829b49014d01158ba379a5..6efee1a8d601de7f0148d9521a1ada587b097385 100644
--- a/src/gui/mainwindow.cpp
+++ b/src/gui/mainwindow.cpp
@@ -383,6 +383,8 @@ MainWindow::MainWindow(ItemFactory *itemFactory, QWidget *parent)
     ui->tabWidget->addToolBars(this);
     addToolBar(Qt::RightToolBarArea, ui->toolBar);
 
+    ui->dockWidgetItemPreview->hide();
+
     WindowGeometryGuard::create(this);
     restoreState( mainWindowState(objectName()) );
     // NOTE: QWidget::isVisible() returns false if parent is not visible.
diff --git a/src/tests/test_utils.h b/src/tests/test_utils.h
index b28a733bbcb833a11eb3e19caf9e2901087c151d..c16b7d76104c2c6b3e7d7b6459569d1b3de4425b 100644
--- a/src/tests/test_utils.h
+++ b/src/tests/test_utils.h
@@ -21,7 +21,7 @@
 #define TEST_UTILS_H
 
 #include <QByteArray>
-#include <QDebug>
+#include <QFile>
 #include <QString>
 #include <QStringList>
 #include <QTest>
@@ -35,7 +35,10 @@
 do { \
     QByteArray errors_ = (ERRORS_OR_EMPTY); \
     if (!errors_.isEmpty()) { \
-      qWarning() << errors_; \
+      QFile ferr; \
+      ferr.open(stderr, QIODevice::WriteOnly); \
+      ferr.write(errors_ + "\n"); \
+      ferr.close(); \
       QVERIFY2(false, "Failed with errors above."); \
     } \
 } while (false)
diff --git a/src/tests/tests.cpp b/src/tests/tests.cpp
index 3762cf81b9f71680d82ec4a496f073d9b2cffa17..51e09190a4ed574de0d3b18dd03a6966cd6e5f4a 100644
--- a/src/tests/tests.cpp
+++ b/src/tests/tests.cpp
@@ -33,6 +33,7 @@
 
 #include <QApplication>
 #include <QClipboard>
+#include <QDebug>
 #include <QDir>
 #include <QFileInfo>
 #include <QMap>