From fbfa825535b52686989ec997766acfc74a4803ed Mon Sep 17 00:00:00 2001
From: Friedemann Kleint <Friedemann.Kleint@nokia.com>
Date: Wed, 14 Apr 2010 15:53:35 +0200
Subject: [PATCH] Symbian/Linux: Remove GnuPoc autodetection code

and make it possible to configure it in the settings.
Split up S60Devices up into a class hierarchy and implement
the Autodetected Windows case and the manually configured
Linux case separately for code clarity. Same with the settings
widgets.
Reviewed-by: con
---
 .../qt-s60/gccetoolchain.cpp                  |   2 +-
 .../qt4projectmanager/qt-s60/s60devices.cpp   | 333 +++++++++++++-----
 .../qt4projectmanager/qt-s60/s60devices.h     |  99 +++++-
 .../qt-s60/s60devicespreferencepane.cpp       | 330 +++++++++++++++--
 .../qt-s60/s60devicespreferencepane.h         |  80 ++++-
 .../qt-s60/s60devicespreferencepane.ui        | 159 +++++----
 .../qt4projectmanager/qt-s60/s60manager.cpp   |   5 +-
 7 files changed, 793 insertions(+), 215 deletions(-)

diff --git a/src/plugins/qt4projectmanager/qt-s60/gccetoolchain.cpp b/src/plugins/qt4projectmanager/qt-s60/gccetoolchain.cpp
index 7ae453be27d..8b73e3633b7 100644
--- a/src/plugins/qt4projectmanager/qt-s60/gccetoolchain.cpp
+++ b/src/plugins/qt4projectmanager/qt-s60/gccetoolchain.cpp
@@ -51,7 +51,7 @@ static QString gcceCommand(const QString &dir)
     gcce += QLatin1String(".exe");
 #endif
     const QString rc = env.searchInPath(gcce);
-    if (rc.isEmpty()) {
+    if (debug && rc.isEmpty()) {
         const QString msg = QString::fromLatin1("GCCEToolChain: Unable to locate '%1' in '%2' (GCCE root: '%3')")
                             .arg(gcce, env.value(QLatin1String("PATH")), dir);
         qWarning("%s", qPrintable(msg));
diff --git a/src/plugins/qt4projectmanager/qt-s60/s60devices.cpp b/src/plugins/qt4projectmanager/qt-s60/s60devices.cpp
index 5e159c4c06a..3b4fe3da23b 100644
--- a/src/plugins/qt4projectmanager/qt-s60/s60devices.cpp
+++ b/src/plugins/qt4projectmanager/qt-s60/s60devices.cpp
@@ -31,6 +31,7 @@
 #include "gccetoolchain.h"
 
 #include <projectexplorer/environment.h>
+#include <coreplugin/icore.h>
 
 #include <QtCore/QSettings>
 #include <QtCore/QXmlStreamReader>
@@ -51,16 +52,36 @@ namespace {
     const char * const DEVICE_DEFAULT = "default";
     const char * const DEVICE_EPOCROOT = "epocroot";
     const char * const DEVICE_TOOLSROOT = "toolsroot";
+    const char * const GNUPOC_SETTINGS_GROUP = "GnuPocSDKs";
+    const char * const AUTODETECT_SETTINGS_GROUP = "SymbianSDKs";
+    const char * const SDK_QT_ASSOC_SETTINGS_KEY_ROOT = "SymbianSDK";
+    const char * const SETTINGS_DEFAULT_SDK_POSTFIX = ",default";
 }
 
 namespace Qt4ProjectManager {
 namespace Internal {
 
+static int findDefaultDevice(const QList<S60Devices::Device> &d)
+{
+    const int count = d.size();
+    for (int i = 0; i < count; i++)
+        if (d.at(i).isDefault)
+            return i;
+    return -1;
+}
+
 S60Devices::Device::Device() :
     isDefault(false)
 {
 }
 
+bool S60Devices::Device::equals(const Device &rhs) const
+{
+    return id == rhs.id && name == rhs.name && isDefault == rhs.isDefault
+           && epocRoot == rhs.epocRoot && toolsRoot == rhs.toolsRoot
+           && qt == rhs.qt;
+}
+
 QString S60Devices::Device::toHtml() const
 {
     QString rc;
@@ -79,60 +100,180 @@ QString S60Devices::Device::toHtml() const
     return rc;
 }
 
-S60Devices::S60Devices(QObject *parent)
-        : QObject(parent)
+// ------------ S60Devices
+S60Devices::S60Devices(QObject *parent) : QObject(parent)
 {
 }
 
-// GNU-Poc stuff
-static const char *epocRootC = "EPOCROOT";
+QList<S60Devices::Device> S60Devices::devices() const
+{
+    return m_devices;
+}
 
-static inline QString msgEnvVarNotSet(const char *var)
+void S60Devices::setDevices(const QList<Device> &devices)
 {
-    return QString::fromLatin1("The environment variable %1 is not set.").arg(QLatin1String(var));
+    if (m_devices != devices) {
+        m_devices = devices;
+        // Ensure a default device
+        if (!m_devices.isEmpty() && findDefaultDevice(m_devices) == -1)
+            m_devices.front().isDefault = true;
+        writeSettings();
+        emit qtVersionsChanged();
+    }
 }
 
-static inline QString msgEnvVarDirNotExist(const QString &dir, const char *var)
+const QList<S60Devices::Device> &S60Devices::devicesList() const
 {
-    return QString::fromLatin1("The directory %1 pointed to by the environment variable %2 is not set.").arg(dir, QLatin1String(var));
+    return m_devices;
 }
 
-bool S60Devices::readLinux()
+QList<S60Devices::Device> &S60Devices::devicesList()
 {
-    // Detect GNUPOC_ROOT/EPOC ROOT
-    const QByteArray epocRootA = qgetenv(epocRootC);
-    if (epocRootA.isEmpty()) {
-        m_errorString = msgEnvVarNotSet(epocRootC);
-        return false;
-    }
+    return m_devices;
+}
 
-    const QDir epocRootDir(QString::fromLocal8Bit(epocRootA));
-    if (!epocRootDir.exists()) {
-        m_errorString = msgEnvVarDirNotExist(epocRootDir.absolutePath(), epocRootC);
-        return false;
-    }
+S60Devices *S60Devices::createS60Devices(QObject *parent)
+{
+    S60Devices *rc = 0;
+#ifdef Q_OS_WIN
+    AutoDetectS60QtDevices *ad = new AutoDetectS60QtDevices(parent);
+    ad->detectDevices();
+    rc = ad;
+#else
+    rc = new GnuPocS60Devices(parent);
+#endif
+    rc->readSettings();
+    return rc;
+}
 
-    // Check Qt
-    Device device;
-    device.id = device.name = QLatin1String("GnuPoc");
-    device.toolsRoot = device.epocRoot = epocRootDir.absolutePath();
-    device.isDefault = true;
-    m_devices.push_back(device);
-    return true;
+int S60Devices::findById(const QString &id) const
+{
+    const int count = m_devices.size();
+    for (int i = 0; i < count; i++)
+        if (m_devices.at(i).id == id)
+            return i;
+    return -1;
 }
 
-bool S60Devices::read()
+int S60Devices::findByEpocRoot(const QString &er) const
 {
-    m_devices.clear();
-    m_errorString.clear();
+    const int count = m_devices.size();
+    for (int i = 0; i < count; i++)
+        if (m_devices.at(i).epocRoot == er)
+            return i;
+    return -1;
+}
+
+S60Devices::Device S60Devices::deviceForId(const QString &id) const
+{
+    const int index = findById(id);
+    return index == -1 ? Device() : m_devices.at(index);
+}
+
+S60Devices::Device S60Devices::deviceForEpocRoot(const QString &root) const
+{
+    const int index = findByEpocRoot(root);
+    return index == -1 ? Device() : m_devices.at(index);
+}
+
+S60Devices::Device S60Devices::defaultDevice() const
+{
+    const int index = findDefaultDevice(m_devices);
+    return index == -1 ? Device() : m_devices.at(index);
+}
+
+QString S60Devices::cleanedRootPath(const QString &deviceRoot)
+{
+    QString path = deviceRoot;
 #ifdef Q_OS_WIN
-    return readWin();
-#else
-    return readLinux();
+    // sbsv2 actually recommends having the DK on a separate drive...
+    // But qmake breaks when doing that!
+    if (path.size() > 1 && path.at(1) == QChar(':'))
+        path = path.mid(2);
 #endif
+
+    if (!path.size() || path.at(path.size()-1) != '/')
+        path.append('/');
+    return path;
+}
+
+S60Devices::StringStringPairList S60Devices::readSdkQtAssociationSettings(const QSettings *settings,
+                                                                          const QString &group,
+                                                                          int *defaultIndexPtr)
+{
+    StringStringPairList rc;
+    // Read out numbered pairs of EpocRoot/QtDir as many as exist
+    // "SymbianSDK1=/epoc,/qt[,default]".
+    const QChar separator = QLatin1Char(',');
+    const QString keyRoot = group + QLatin1Char('/') + QLatin1String(SDK_QT_ASSOC_SETTINGS_KEY_ROOT);
+    int defaultIndex = -1;
+    for (int i = 1; ; i++) {
+        // Split pairs of epocroot/qtdir.
+        const QVariant valueV = settings->value(keyRoot + QString::number(i));
+        if (!valueV.isValid())
+            break;
+        // Check for default postfix
+        QString value = valueV.toString();
+        if (value.endsWith(QLatin1String(SETTINGS_DEFAULT_SDK_POSTFIX))) {
+            value.truncate(value.size() - qstrlen(SETTINGS_DEFAULT_SDK_POSTFIX));
+            defaultIndex = rc.size();
+        }
+        // Split into SDK and Qt
+        const int separatorPos = value.indexOf(separator);
+        if (separatorPos == -1)
+            break;
+        const QString epocRoot = value.left(separatorPos);
+        const QString qtDir = value.mid(separatorPos + 1);
+        rc.push_back(StringStringPair(epocRoot, qtDir));
+    }
+    if (defaultIndexPtr)
+        *defaultIndexPtr = defaultIndex;
+    return rc;
+}
+
+void S60Devices::writeSdkQtAssociationSettings(QSettings *settings, const QString &group) const
+{
+    // Write out as numbered pairs of EpocRoot/QtDir and indicate default
+    // "SymbianSDK1=/epoc,/qt[,default]".
+    settings->beginGroup(group);
+    settings->remove(QString()); // remove all keys
+    if (const int count = devicesList().size()) {
+        const QString keyRoot = QLatin1String(SDK_QT_ASSOC_SETTINGS_KEY_ROOT);
+        const QChar separator = QLatin1Char(',');
+        for (int i = 0; i < count; i++) {
+            const QString key = keyRoot + QString::number(i + 1);
+            QString value = devicesList().at(i).epocRoot;
+            value += separator;
+            value += devicesList().at(i).qt;
+            // Indicate default by postfix ",default"
+            if (devicesList().at(i).isDefault)
+                value += QLatin1String(SETTINGS_DEFAULT_SDK_POSTFIX);
+            settings->setValue(key, value);
+        }
+    }
+    settings->endGroup();
+}
+
+void S60Devices::readSettings()
+{
+}
+
+void S60Devices::writeSettings()
+{
+}
+
+// ------------------ S60Devices
+
+AutoDetectS60Devices::AutoDetectS60Devices(QObject *parent) :
+        S60Devices(parent)
+{
+}
+
+QString AutoDetectS60Devices::errorString() const
+{
+    return m_errorString;
 }
 
-// Windows: Get list of paths containing common program data
 // as pointed to by environment.
 static QStringList commonProgramFilesPaths()
 {
@@ -147,8 +288,6 @@ static QStringList commonProgramFilesPaths()
     return rc;
 }
 
-// Windows EPOC
-
 // Find the "devices.xml" file containing the SDKs
 static QString devicesXmlFile(QString *errorMessage)
 {
@@ -174,8 +313,10 @@ static QString devicesXmlFile(QString *errorMessage)
     return QString();
 }
 
-bool S60Devices::readWin()
+bool AutoDetectS60Devices::detectDevices()
 {
+    devicesList().clear();
+    m_errorString.clear();
     const QString devicesXmlPath = devicesXmlFile(&m_errorString);
     if (devicesXmlPath.isEmpty())
         return false;
@@ -210,7 +351,7 @@ bool S60Devices::readWin()
                         }
                         if (device.toolsRoot.isEmpty())
                             device.toolsRoot = device.epocRoot;
-                        m_devices.append(device);
+                        devicesList().append(device);
                     }
                 }
             } else {
@@ -227,6 +368,35 @@ bool S60Devices::readWin()
     return true;
 }
 
+void AutoDetectS60Devices::readSettings()
+{
+    // Read the associated Qt version from the settings
+    // and set on the autodetected SDKs.
+    bool changed = false;
+    const QSettings *settings = Core::ICore::instance()->settings();
+    foreach (const StringStringPair &p, readSdkQtAssociationSettings(settings, QLatin1String(GNUPOC_SETTINGS_GROUP))) {
+        const int index = findByEpocRoot(p.first);
+        if (index != -1 && devicesList().at(index).qt != p.second) {
+            devicesList()[index].qt = p.second;
+            changed = true;
+        }
+    }
+    if (changed)
+        emit qtVersionsChanged();
+}
+
+void AutoDetectS60Devices::writeSettings()
+{
+    writeSdkQtAssociationSettings(Core::ICore::instance()->settings(), QLatin1String(AUTODETECT_SETTINGS_GROUP));
+}
+
+// ========== AutoDetectS60QtDevices
+
+AutoDetectS60QtDevices::AutoDetectS60QtDevices(QObject *parent) :
+        AutoDetectS60Devices(parent)
+{
+}
+
 // Detect a Qt version that is installed into a Symbian SDK
 static QString detect_SDK_installedQt(const QString &epocRoot)
 {
@@ -263,28 +433,14 @@ static QString detect_SDK_installedQt(const QString &epocRoot)
     return QDir(QString::fromLatin1(buffer.mid(index, lastIndex-index))).absolutePath();
 }
 
-// GnuPoc: Detect a Qt version that is symlinked/below an SDK
-// TODO: Find a proper way of doing that
-static QString detectGnuPocQt(const QString &epocRoot)
-{
-    const QFileInfo fi(epocRoot + QLatin1String("/qt"));
-    if (!fi.exists())
-        return QString();
-    if (fi.isSymLink())
-        return QFileInfo(fi.symLinkTarget()).absoluteFilePath();
-    return fi.absoluteFilePath();
-}
-
-bool S60Devices::detectQtForDevices()
+bool AutoDetectS60QtDevices::detectQtForDevices()
 {
     bool changed = false;
-    const int deviceCount = m_devices.size();
+    const int deviceCount = devicesList().size();
     for (int i = 0; i < deviceCount; ++i) {
-        Device &device = m_devices[i];
+        Device &device = devicesList()[i];
         if (device.qt.isEmpty()) {
             device.qt = detect_SDK_installedQt(device.epocRoot);
-            if (device.qt.isEmpty())
-                device.qt = detectGnuPocQt(device.epocRoot);
             if (device.qt.isEmpty()) {
                 qWarning("Unable to detect Qt version for '%s'.", qPrintable(device.epocRoot));
             } else {
@@ -297,59 +453,50 @@ bool S60Devices::detectQtForDevices()
     return true;
 }
 
-QString S60Devices::errorString() const
-{
-    return m_errorString;
-}
-
-QList<S60Devices::Device> S60Devices::devices() const
+bool AutoDetectS60QtDevices::detectDevices()
 {
-    return m_devices;
+    return AutoDetectS60Devices::detectDevices() && detectQtForDevices();
 }
 
-S60Devices::Device S60Devices::deviceForId(const QString &id) const
+// ------- GnuPocS60Devices
+GnuPocS60Devices::GnuPocS60Devices(QObject *parent) :
+        S60Devices(parent)
 {
-    foreach (const S60Devices::Device &i, m_devices) {
-        if (i.id == id) {
-            return i;
-        }
-    }
-    return Device();
 }
 
-S60Devices::Device S60Devices::deviceForEpocRoot(const QString &root) const
+S60Devices::Device GnuPocS60Devices::createDevice(const QString &epoc, const QString &qtDir)
 {
-    foreach (const S60Devices::Device &i, m_devices) {
-        if (i.epocRoot == root) {
-            return i;
-        }
-    }
-    return Device();
+    Device device;
+    device.id = device.name = QLatin1String("GnuPoc");
+    device.toolsRoot = device.epocRoot = epoc;
+    device.qt = qtDir;
+    return device;
 }
 
-S60Devices::Device S60Devices::defaultDevice() const
+// GnuPoc settings are just the pairs of EpocRoot and Qt Dir.
+void GnuPocS60Devices::readSettings()
 {
-    foreach (const S60Devices::Device &i, m_devices) {
-        if (i.isDefault) {
-            return i;
+    // Read out numbered pairs of EpocRoot/QtDir as many as exist
+    // "SymbianSDK1=/epoc,/qt".
+    devicesList().clear();
+    int defaultIndex = 0;
+    const QSettings *settings = Core::ICore::instance()->settings();
+    const StringStringPairList devices =readSdkQtAssociationSettings(settings, QLatin1String(GNUPOC_SETTINGS_GROUP), &defaultIndex);
+    foreach (const StringStringPair &p, devices)
+        devicesList().append(createDevice(p.first, p.second));
+    // Ensure a default
+    if (!devicesList().isEmpty()) {
+        if (defaultIndex >= 0 && defaultIndex < devicesList().size()) {
+            devicesList()[defaultIndex].isDefault = true;
+        } else {
+            devicesList().front().isDefault = true;
         }
     }
-    return Device();
 }
 
-QString S60Devices::cleanedRootPath(const QString &deviceRoot)
+void GnuPocS60Devices::writeSettings()
 {
-    QString path = deviceRoot;
-#ifdef Q_OS_WIN
-    // sbsv2 actually recommends having the DK on a separate drive...
-    // But qmake breaks when doing that!
-    if (path.size() > 1 && path.at(1) == QChar(':'))
-        path = path.mid(2);
-#endif
-
-    if (!path.size() || path.at(path.size()-1) != '/')
-        path.append('/');
-    return path;
+    writeSdkQtAssociationSettings(Core::ICore::instance()->settings(), QLatin1String(GNUPOC_SETTINGS_GROUP));
 }
 
 // S60ToolChainMixin
diff --git a/src/plugins/qt4projectmanager/qt-s60/s60devices.h b/src/plugins/qt4projectmanager/qt-s60/s60devices.h
index 4e3657f5527..076c81184c2 100644
--- a/src/plugins/qt4projectmanager/qt-s60/s60devices.h
+++ b/src/plugins/qt4projectmanager/qt-s60/s60devices.h
@@ -35,20 +35,25 @@
 #include <QtCore/QObject>
 #include <QtCore/QString>
 #include <QtCore/QList>
+#include <QtCore/QPair>
 
 QT_BEGIN_NAMESPACE
 class QDebug;
+class QSettings;
 QT_END_NAMESPACE
 
 namespace Qt4ProjectManager {
 namespace Internal {
 
+// List of S60 devices.
 class S60Devices : public QObject
 {
     Q_OBJECT
 public:
     struct Device {
         Device();
+        bool equals(const Device &rhs) const;
+
         QString toHtml() const;
 
         QString id;
@@ -59,26 +64,106 @@ public:
         QString qt;
     };
 
-    S60Devices(QObject *parent = 0);
-    bool read();
-    QString errorString() const;
+    // Construct a devices object, does autodetection if applicable
+    // and restores settings.
+    static S60Devices *createS60Devices(QObject *parent);
+
     QList<Device> devices() const;
-    bool detectQtForDevices();
+    // Set devices, write settings and emit changed signals accordingly.
+    void setDevices(const QList<Device> &device);
+
     Device deviceForId(const QString &id) const;
     Device deviceForEpocRoot(const QString &root) const;
     Device defaultDevice() const;
 
+    int findByEpocRoot(const QString &er) const;
+
     static QString cleanedRootPath(const QString &deviceRoot);
+
 signals:
     void qtVersionsChanged();
 
+protected:
+    typedef QPair<QString, QString> StringStringPair;
+    typedef QList<StringStringPair> StringStringPairList;
+
+    explicit S60Devices(QObject *parent = 0);
+
+    const QList<Device> &devicesList() const;
+    QList<Device> &devicesList();
+
+    int findById(const QString &id) const;
+
+    // Helpers to serialize the association of Symbian SDK<->Qt
+    // to QSettings (pair of SDK/Qt).
+    static StringStringPairList readSdkQtAssociationSettings(const QSettings *settings,
+                                                             const QString &group,
+                                                             int *defaultIndex = 0);
+    void writeSdkQtAssociationSettings(QSettings *settings, const QString &group) const;
+
 private:
-    bool readLinux();
-    bool readWin();
-    QString m_errorString;
+    virtual void readSettings(); // empty stubs
+    virtual void writeSettings();
+
     QList<Device> m_devices;
 };
 
+inline bool operator==(const S60Devices::Device &d1, const S60Devices::Device &d2)
+{ return d1.equals(d2); }
+inline bool operator!=(const S60Devices::Device &d1, const S60Devices::Device &d2)
+{ return !d1.equals(d2); }
+
+// Autodetected Symbian Devices (as parsed from devices.xml file on Windows)
+// with a manually set version of Qt. Currently not used, but might be by
+// makefile-based builds on Windows.
+class AutoDetectS60Devices : public S60Devices
+{
+    Q_OBJECT
+public:
+    explicit AutoDetectS60Devices(QObject *parent = 0);
+    virtual bool detectDevices();
+    QString errorString() const;
+
+private:
+    // Write and restore Qt-SDK associations.
+    virtual void readSettings();
+    virtual void writeSettings();
+
+    QString m_errorString;
+};
+
+// Autodetected Symbian-Qt-Devices (with Qt installed
+// into the SDK) for ABLD, Raptor. Completely autodetected.
+class AutoDetectS60QtDevices : public AutoDetectS60Devices
+{
+    Q_OBJECT
+public:
+    explicit AutoDetectS60QtDevices(QObject *parent = 0);
+    // Overwritten to detect associated Qt versions in addition.
+    virtual bool detectDevices();
+
+private:
+    // No settings as it is completely autodetected.
+    virtual void readSettings()  {}
+    virtual void writeSettings() {}
+
+    bool detectQtForDevices();
+};
+
+// Manually configured Symbian Devices completely based on QSettings.
+class GnuPocS60Devices : public S60Devices
+{
+    Q_OBJECT
+public:
+    explicit GnuPocS60Devices(QObject *parent = 0);
+
+    static Device createDevice(const QString &epoc, const QString &qtDir);
+
+private:
+    virtual void readSettings();
+    virtual void writeSettings();
+};
+
 /* Mixin for the toolchains with convenience functions for EPOC
  * (Windows) and GnuPoc (Linux). */
 
diff --git a/src/plugins/qt4projectmanager/qt-s60/s60devicespreferencepane.cpp b/src/plugins/qt4projectmanager/qt-s60/s60devicespreferencepane.cpp
index db5b54c1d50..cabb8c1eae6 100644
--- a/src/plugins/qt4projectmanager/qt-s60/s60devicespreferencepane.cpp
+++ b/src/plugins/qt4projectmanager/qt-s60/s60devicespreferencepane.cpp
@@ -34,50 +34,207 @@
 #include <qt4projectmanager/qt4projectmanagerconstants.h>
 
 #include <utils/qtcassert.h>
+#include <coreplugin/coreconstants.h>
 
 #include <QtCore/QDir>
 #include <QtCore/QtDebug>
+#include <QtCore/QSharedPointer>
 
-using namespace Qt4ProjectManager;
-using namespace Qt4ProjectManager::Internal;
+#include <QtGui/QFileDialog>
+#include <QtGui/QMessageBox>
+#include <QtGui/QIcon>
+#include <QtGui/QApplication>
+#include <QtGui/QStyle>
+#include <QtGui/QStandardItemModel>
+#include <QtGui/QStandardItem>
 
-S60DevicesWidget::S60DevicesWidget(QWidget *parent, S60Devices *devices) :
+enum { deviceRole = Qt::UserRole + 1 };
+
+enum Columns { DefaultColumn, EpocColumn, QtColumn, ColumnCount };
+
+typedef QSharedPointer<Qt4ProjectManager::Internal::S60Devices::Device> DevicePtr;
+Q_DECLARE_METATYPE(DevicePtr)
+
+typedef QList<QStandardItem *> StandardItemList;
+
+namespace Qt4ProjectManager {
+namespace Internal {
+
+static inline DevicePtr deviceFromItem(const QStandardItem *item)
+{
+    return qvariant_cast<DevicePtr>(item->data(deviceRole));
+}
+
+// Device model storing a shared pointer to the device as user data.
+// Provides a checkable 'default' column which works exclusively.
+class S60DevicesModel : public QStandardItemModel {
+    Q_OBJECT
+public:
+    typedef QList<S60Devices::Device> DeviceList;
+
+    explicit S60DevicesModel(bool defaultColumnCheckable, QObject *parent = 0);
+
+    void setDevices(const DeviceList &list);
+    DeviceList devices() const;
+    void appendDevice(const S60Devices::Device &device);
+
+private slots:
+    void slotItemChanged(QStandardItem *item);
+
+private:
+    const bool m_defaultColumnCheckable;
+};
+
+S60DevicesModel::S60DevicesModel(bool defaultColumnCheckable, QObject *parent) :
+        QStandardItemModel(0, ColumnCount, parent),
+        m_defaultColumnCheckable(defaultColumnCheckable)
+{
+    QStringList headers;
+    headers << S60DevicesBaseWidget::tr("Default")
+            << S60DevicesBaseWidget::tr("SDK Location")
+            << S60DevicesBaseWidget::tr("Qt Location");
+    setHorizontalHeaderLabels(headers);
+
+    if (m_defaultColumnCheckable)
+        connect(this, SIGNAL(itemChanged(QStandardItem*)),
+                this, SLOT(slotItemChanged(QStandardItem*)));
+}
+
+void S60DevicesModel::appendDevice(const S60Devices::Device &device)
+{
+    // Create SDK/Qt column items with shared pointer to entry as data.
+    const QVariant deviceData = qVariantFromValue(DevicePtr(new S60Devices::Device(device)));
+
+    const Qt::ItemFlags flags = Qt::ItemIsEnabled|Qt::ItemIsSelectable;
+
+    QStandardItem *defaultItem = new QStandardItem;
+    defaultItem->setCheckable(true);
+    defaultItem->setCheckState(device.isDefault ? Qt::Checked : Qt::Unchecked);
+    // Item is only checkable if it is not the default.
+    Qt::ItemFlags checkFlags = flags;
+    if (!device.isDefault && m_defaultColumnCheckable)
+        checkFlags |= Qt::ItemIsUserCheckable;
+    defaultItem->setFlags(checkFlags);
+
+    defaultItem->setData(deviceData);
+
+    QStandardItem *epocItem = new QStandardItem(QDir::toNativeSeparators(device.epocRoot));
+    epocItem->setFlags(flags);
+    epocItem->setData(deviceData);
+
+    const QString qtDesc = device.qt.isEmpty() ?
+                           S60DevicesModel::tr("No Qt installed") :
+                           QDir::toNativeSeparators(device.qt);
+    QStandardItem *qtItem = new QStandardItem(qtDesc);
+    qtItem->setFlags(flags);
+    qtItem->setData(deviceData);
+
+    const QString tooltip = device.toHtml();
+    epocItem->setToolTip(tooltip);
+    qtItem->setToolTip(tooltip);
+
+    StandardItemList row;
+    row << defaultItem << epocItem << qtItem;
+    appendRow(row);
+}
+
+void S60DevicesModel::setDevices(const DeviceList &list)
+{
+    removeRows(0, rowCount());
+    foreach(const S60Devices::Device &device, list)
+        appendDevice(device);
+}
+
+S60DevicesModel::DeviceList S60DevicesModel::devices() const
+{
+    S60DevicesModel::DeviceList rc;
+    const int count = rowCount();
+    for (int r = 0; r < count; r++)
+        rc.push_back(S60Devices::Device(*deviceFromItem(item(r, 0))));
+    return rc;
+}
+
+void S60DevicesModel::slotItemChanged(QStandardItem *changedItem)
+{
+    // Sync all "default" checkmarks. Emulate an exclusive group
+    // by enabling only the unchecked items (preventing the user from unchecking)
+    // and uncheck all other items. Protect against recursion.
+    if (changedItem->column() != DefaultColumn || changedItem->checkState() != Qt::Checked)
+        return;
+    const int row = changedItem->row();
+    const int count = rowCount();
+    for (int r = 0; r < count; r++) {
+        QStandardItem *rowItem = item(r, DefaultColumn);
+        if (r == row) { // Prevent uncheck.
+            rowItem->setFlags(rowItem->flags() & ~Qt::ItemIsUserCheckable);
+            deviceFromItem(rowItem)->isDefault = true;
+        } else {
+            // Uncheck others.
+            rowItem->setCheckState(Qt::Unchecked);
+            rowItem->setFlags(rowItem->flags() | Qt::ItemIsUserCheckable);
+            deviceFromItem(rowItem)->isDefault = false;
+        }
+    }
+}
+
+// --------------- S60DevicesBaseWidget
+S60DevicesBaseWidget::S60DevicesBaseWidget(unsigned flags, QWidget *parent) :
     QWidget(parent),
     m_ui(new Ui::S60DevicesPreferencePane),
-    m_devices(devices)
+    m_model(new S60DevicesModel(flags & DeviceDefaultCheckable))
 {
     m_ui->setupUi(this);
-    connect(m_ui->refreshButton, SIGNAL(clicked()), this, SLOT(updateDevices()));
-    updateDevicesList();
+    m_ui->addButton->setIcon(QIcon(QLatin1String(Core::Constants::ICON_PLUS)));
+    connect(m_ui->addButton, SIGNAL(clicked()), this, SLOT(addDevice()));
+    m_ui->removeButton->setIcon(QIcon(QLatin1String(Core::Constants::ICON_MINUS)));
+    connect(m_ui->removeButton, SIGNAL(clicked()), this, SLOT(removeDevice()));
+    m_ui->refreshButton->setIcon(qApp->style()->standardIcon(QStyle::SP_BrowserReload));
+    connect(m_ui->refreshButton, SIGNAL(clicked()), this, SLOT(refresh()));
+    m_ui->changeQtButton->setIcon(QIcon(QLatin1String(":/welcome/images/qt_logo.png")));
+    connect(m_ui->changeQtButton, SIGNAL(clicked()), this, SLOT(changeQtVersion()));
+
+    m_ui->list->setModel(m_model);
+    connect(m_ui->list->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)),
+            this, SLOT(currentChanged(QModelIndex,QModelIndex)));
+
+    m_ui->addButton->setVisible(flags & ShowAddButton);
+    m_ui->removeButton->setVisible(flags & ShowAddButton);
+    m_ui->removeButton->setEnabled(false);
+    m_ui->changeQtButton->setVisible(flags & ShowChangeQtButton);
+    m_ui->removeButton->setEnabled(false);
+    m_ui->refreshButton->setVisible(flags & ShowRefreshButton);
 }
 
-S60DevicesWidget::~S60DevicesWidget()
+S60DevicesBaseWidget::~S60DevicesBaseWidget()
 {
     delete m_ui;
 }
 
-void S60DevicesWidget::updateDevices()
+QStandardItem *S60DevicesBaseWidget::currentItem() const
 {
-    m_devices->read();
-    QTC_ASSERT(m_devices->detectQtForDevices(), return);
-    updateDevicesList();
+    // Return the column-0 item.
+    QModelIndex current = m_ui->list->currentIndex();
+    if (current.isValid()) {
+        if (current.row() != 0)
+            current = current.sibling(current.row(), 0);
+        return m_model->itemFromIndex(current);
+    }
+    return 0;
 }
 
-void S60DevicesWidget::updateDevicesList()
+S60DevicesBaseWidget::DeviceList S60DevicesBaseWidget::devices() const
 {
-    QList<S60Devices::Device> devices = m_devices->devices();
-    m_ui->list->clear();
-    foreach (const S60Devices::Device &device, devices) {
-        QStringList columns;
-        columns << QDir::toNativeSeparators(device.epocRoot)
-                << (device.qt.isEmpty()?tr("No Qt installed"):QDir::toNativeSeparators(device.qt));
-        QTreeWidgetItem *item = new QTreeWidgetItem(columns);
-        const QString tooltip = device.toHtml();
-        item->setToolTip(0, tooltip);
-        item->setToolTip(1, tooltip);
-        m_ui->list->addTopLevelItem(item);
-    }
-    const QString errorString = m_devices->errorString();
+    return m_model->devices();
+}
+
+void S60DevicesBaseWidget::setDevices(const DeviceList &s60devices,
+                                      const QString &errorString)
+{
+    m_model->setDevices(s60devices);
+
+    for (int c = 0; c < ColumnCount; c++)
+        m_ui->list->resizeColumnToContents(c);
+
     if (errorString.isEmpty()) {
         clearErrorLabel();
     } else {
@@ -85,24 +242,114 @@ void S60DevicesWidget::updateDevicesList()
     }
 }
 
-void S60DevicesWidget::setErrorLabel(const QString& t)
+void S60DevicesBaseWidget::changeQtVersion()
+{
+    if (const QStandardItem *item = currentItem()) {
+        const QString qtDir = promptDirectory(tr("Choose Qt folder"));
+        if (!qtDir.isEmpty()) {
+            const DevicePtr device = deviceFromItem(item);
+            device->qt = qtDir;
+        }
+    }
+}
+
+void S60DevicesBaseWidget::removeDevice()
+{
+    if (const QStandardItem *item = currentItem())
+        m_model->removeRows(item->row(), 1);
+}
+
+void S60DevicesBaseWidget::currentChanged(const QModelIndex &current,
+                                          const QModelIndex & /* previous */)
+{
+    const bool hasItem = current.isValid();
+    m_ui->changeQtButton->setEnabled(hasItem);
+    m_ui->removeButton->setEnabled(hasItem);
+}
+
+void S60DevicesBaseWidget::setErrorLabel(const QString& t)
 {
     m_ui->errorLabel->setText(t);
     m_ui->errorLabel->setVisible(true);
 }
 
-void S60DevicesWidget::clearErrorLabel()
+void S60DevicesBaseWidget::clearErrorLabel()
 {
     m_ui->errorLabel->setVisible(false);
 }
 
+QString S60DevicesBaseWidget::promptDirectory(const QString &title)
+{
+    return QFileDialog::getExistingDirectory(this, title);
+}
+
+void S60DevicesBaseWidget::appendDevice(const S60Devices::Device &d)
+{
+    m_model->appendDevice(d);
+}
+
+int S60DevicesBaseWidget::deviceCount() const
+{
+    return m_model->rowCount();
+}
+
+// ============ AutoDetectS60DevicesWidget
 
+AutoDetectS60DevicesWidget::AutoDetectS60DevicesWidget(QWidget *parent,
+                                                       AutoDetectS60Devices *devices,
+                                                       bool changeQtVersionEnabled) :
+    S60DevicesBaseWidget(ShowRefreshButton | (changeQtVersionEnabled ? unsigned(ShowChangeQtButton) : 0u),
+                         parent),
+    m_devices(devices)
+{
+    refresh();
+}
+
+void AutoDetectS60DevicesWidget::refresh()
+{
+    m_devices->detectDevices();
+    setDevices(m_devices->devices(), m_devices->errorString());
+}
+
+// ============ GnuPocS60DevicesWidget
+GnuPocS60DevicesWidget::GnuPocS60DevicesWidget(QWidget *parent) :
+    S60DevicesBaseWidget(ShowAddButton|ShowRemoveButton|ShowChangeQtButton|DeviceDefaultCheckable,
+                         parent)
+{
+}
+
+void GnuPocS60DevicesWidget::addDevice()
+{
+    // 1) Prompt for GnuPoc
+    const QString epocRoot = promptDirectory(tr("Step 1 of 2: Choose GnuPoc folder"));
+    if (epocRoot.isEmpty())
+        return;
+    // 2) Prompt for Qt. Catch equal inputs just in case someone clicks very rapidly.
+    QString qtDir;
+    while (true) {
+        qtDir = promptDirectory(tr("Step 2 of 2: Choose Qt folder"));
+        if (qtDir.isEmpty())
+            return;
+        if (qtDir == epocRoot) {
+            QMessageBox::warning(this, tr("Adding GnuPoc"),
+                                 tr("GnuPoc and Qt folders must not be identical."));
+        } else {
+            break;
+        }
+    }
+    // Add a device, make default if first.
+    S60Devices::Device device = GnuPocS60Devices::createDevice(epocRoot, qtDir);
+    if (deviceCount() == 0)
+        device.isDefault = true;
+    appendDevice(device);
+}
+
+// ================= S60DevicesPreferencePane
 S60DevicesPreferencePane::S60DevicesPreferencePane(S60Devices *devices, QObject *parent)
         : Core::IOptionsPage(parent),
         m_widget(0),
         m_devices(devices)
 {
-    m_devices->read();
 }
 
 S60DevicesPreferencePane::~S60DevicesPreferencePane()
@@ -134,18 +381,43 @@ QIcon S60DevicesPreferencePane::categoryIcon() const
     return QIcon(Constants::QT_SETTINGS_CATEGORY_ICON);
 }
 
+S60DevicesBaseWidget *S60DevicesPreferencePane::createWidget(QWidget *parent) const
+{
+    // Symbian ABLD/Raptor: Qt installed into SDK, cannot change
+    if (AutoDetectS60QtDevices *aqd = qobject_cast<AutoDetectS60QtDevices *>(m_devices))
+        return new AutoDetectS60DevicesWidget(parent, aqd, false);
+    // Not used yet: Manual association of Qt with auto-detected SDK
+    if (AutoDetectS60Devices *ad = qobject_cast<AutoDetectS60Devices *>(m_devices))
+        return new AutoDetectS60DevicesWidget(parent, ad, true);
+    if (GnuPocS60Devices *gd = qobject_cast<GnuPocS60Devices*>(m_devices)) {
+        GnuPocS60DevicesWidget *gw = new GnuPocS60DevicesWidget(parent);
+        gw->setDevices(gd->devices());
+        return gw;
+    }
+    return 0; // Huh?
+}
+
 QWidget *S60DevicesPreferencePane::createPage(QWidget *parent)
 {
     if (m_widget)
         delete m_widget;
-    m_widget = new S60DevicesWidget(parent, m_devices);
+    m_widget = createWidget(parent);
+    QTC_ASSERT(m_widget, return 0)
     return m_widget;
 }
 
 void S60DevicesPreferencePane::apply()
 {
+    QTC_ASSERT(m_widget, return)
+
+    m_devices->setDevices(m_widget->devices());
 }
 
 void S60DevicesPreferencePane::finish()
 {
 }
+
+} // namespace Internal
+} // namespace Qt4ProjectManager
+
+#include "s60devicespreferencepane.moc"
diff --git a/src/plugins/qt4projectmanager/qt-s60/s60devicespreferencepane.h b/src/plugins/qt4projectmanager/qt-s60/s60devicespreferencepane.h
index 312c1efb906..a48ae399510 100644
--- a/src/plugins/qt4projectmanager/qt-s60/s60devicespreferencepane.h
+++ b/src/plugins/qt4projectmanager/qt-s60/s60devicespreferencepane.h
@@ -30,38 +30,96 @@
 #ifndef S60DEVICESPREFERENCEPANE_H
 #define S60DEVICESPREFERENCEPANE_H
 
+#include "s60devices.h"
 #include <coreplugin/dialogs/ioptionspage.h>
 
 #include <QtCore/QPointer>
 #include <QtGui/QWidget>
 
+QT_BEGIN_NAMESPACE
+class QStandardItem;
+class QAbstractButton;
+class QModelIndex;
+QT_END_NAMESPACE
+
 namespace Qt4ProjectManager {
 namespace Internal {
-class S60Devices;
+class AutoDetectS60Devices;
+class GnuPocS60Devices;
+class S60DevicesModel;
 
 namespace Ui {    
     class S60DevicesPreferencePane;
 }
 
-class S60DevicesWidget : public QWidget
+// Base pane listing Symbian SDKs with "Change Qt" version functionality.
+class S60DevicesBaseWidget : public QWidget
 {
     Q_OBJECT
 public:
-    S60DevicesWidget(QWidget *parent, S60Devices *devices);
-    ~S60DevicesWidget();
+    typedef QList<S60Devices::Device> DeviceList;
 
-private slots:
-    void updateDevices();
+    enum Flags { ShowAddButton = 0x1, ShowRemoveButton = 0x2,
+                 ShowChangeQtButton =0x4, ShowRefreshButton = 0x8,
+                 DeviceDefaultCheckable = 0x10 };
+
+    virtual ~S60DevicesBaseWidget();
+
+    DeviceList devices() const;
+    void setDevices(const DeviceList &dl, const QString &errorString = QString());
+
+protected:
+    explicit S60DevicesBaseWidget(unsigned flags, QWidget *parent = 0);
 
-private:
-    void updateDevicesList();
     void setErrorLabel(const QString&);
     void clearErrorLabel();
 
+    QString promptDirectory(const QString &title);
+    void appendDevice(const S60Devices::Device &d);
+    int deviceCount() const;
+
+    QStandardItem *currentItem() const;
+
+private slots:
+    void currentChanged(const QModelIndex &current, const QModelIndex &previous);
+    void changeQtVersion();
+    void removeDevice();
+    virtual void addDevice() {} // Default does nothing
+    virtual void refresh() {} // Default does nothing
+
+private:
     Ui::S60DevicesPreferencePane *m_ui;
-    S60Devices *m_devices;
+    S60DevicesModel *m_model;
 };
 
+// Widget for autodetected SDK's showing a refresh button.
+class AutoDetectS60DevicesWidget : public S60DevicesBaseWidget
+{
+    Q_OBJECT
+public:
+    explicit AutoDetectS60DevicesWidget(QWidget *parent,
+                                        AutoDetectS60Devices *devices,
+                                        bool changeQtVersionEnabled);
+
+private slots:
+    virtual void refresh();
+
+private:
+    AutoDetectS60Devices *m_devices;
+};
+
+// Widget for manually configured SDK's showing a add/remove buttons.
+class GnuPocS60DevicesWidget : public S60DevicesBaseWidget
+{
+    Q_OBJECT
+public:
+    explicit GnuPocS60DevicesWidget(QWidget *parent = 0);
+
+private slots:
+    virtual void addDevice();
+};
+
+// Options Pane.
 class S60DevicesPreferencePane : public Core::IOptionsPage
 {
     Q_OBJECT
@@ -80,7 +138,9 @@ public:
     void finish();
 
 private:
-    QPointer<S60DevicesWidget> m_widget;
+    S60DevicesBaseWidget *createWidget(QWidget *parent) const;
+
+    QPointer<S60DevicesBaseWidget> m_widget;
     S60Devices *m_devices;
 };
 
diff --git a/src/plugins/qt4projectmanager/qt-s60/s60devicespreferencepane.ui b/src/plugins/qt4projectmanager/qt-s60/s60devicespreferencepane.ui
index 6841bd023b1..ccef3f84e14 100644
--- a/src/plugins/qt4projectmanager/qt-s60/s60devicespreferencepane.ui
+++ b/src/plugins/qt4projectmanager/qt-s60/s60devicespreferencepane.ui
@@ -6,86 +6,101 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>274</width>
-    <height>264</height>
+    <width>316</width>
+    <height>241</height>
    </rect>
   </property>
   <property name="windowTitle">
    <string>Form</string>
   </property>
-  <layout class="QVBoxLayout" name="verticalLayout">
+  <layout class="QVBoxLayout" name="verticalLayout_2">
    <item>
-    <widget class="QTreeWidget" name="list">
-     <property name="indentation">
-      <number>0</number>
-     </property>
-     <property name="rootIsDecorated">
-      <bool>false</bool>
-     </property>
-     <property name="uniformRowHeights">
-      <bool>true</bool>
-     </property>
-     <property name="columnCount">
-      <number>2</number>
-     </property>
-     <attribute name="headerCascadingSectionResizes">
-      <bool>true</bool>
-     </attribute>
-     <attribute name="headerCascadingSectionResizes">
-      <bool>true</bool>
-     </attribute>
-     <column>
-      <property name="text">
-       <string>SDK Location</string>
-      </property>
-     </column>
-     <column>
-      <property name="text">
-       <string>Qt Location</string>
-      </property>
-     </column>
+    <widget class="QGroupBox" name="groupBox">
+     <layout class="QVBoxLayout" name="verticalLayout">
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout">
+        <item>
+         <widget class="QTreeView" name="list">
+          <property name="indentation">
+           <number>0</number>
+          </property>
+          <property name="rootIsDecorated">
+           <bool>false</bool>
+          </property>
+          <property name="uniformRowHeights">
+           <bool>true</bool>
+          </property>
+          <property name="allColumnsShowFocus">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <layout class="QVBoxLayout" name="buttonLayout">
+          <item>
+           <widget class="QToolButton" name="refreshButton">
+            <property name="toolTip">
+             <string>Refresh</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QToolButton" name="addButton">
+            <property name="toolTip">
+             <string>Add</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QToolButton" name="changeQtButton">
+            <property name="toolTip">
+             <string>Change Qt version</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QToolButton" name="removeButton">
+            <property name="toolTip">
+             <string>Remove</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <spacer name="verticalSpacer">
+            <property name="orientation">
+             <enum>Qt::Vertical</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>20</width>
+              <height>40</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+         </layout>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <widget class="QLabel" name="errorLabel">
+        <property name="styleSheet">
+         <string notr="true">background-color: red;</string>
+        </property>
+        <property name="text">
+         <string>Error</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+        <property name="textInteractionFlags">
+         <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+        </property>
+       </widget>
+      </item>
+     </layout>
     </widget>
    </item>
-   <item>
-    <widget class="QLabel" name="errorLabel">
-     <property name="styleSheet">
-      <string notr="true">color: red;</string>
-     </property>
-     <property name="text">
-      <string>Error</string>
-     </property>
-     <property name="wordWrap">
-      <bool>true</bool>
-     </property>
-     <property name="textInteractionFlags">
-      <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
-     </property>
-    </widget>
-   </item>
-   <item>
-    <layout class="QHBoxLayout" name="horizontalLayout">
-     <item>
-      <spacer name="horizontalSpacer">
-       <property name="orientation">
-        <enum>Qt::Horizontal</enum>
-       </property>
-       <property name="sizeHint" stdset="0">
-        <size>
-         <width>40</width>
-         <height>20</height>
-        </size>
-       </property>
-      </spacer>
-     </item>
-     <item>
-      <widget class="QPushButton" name="refreshButton">
-       <property name="text">
-        <string>Refresh</string>
-       </property>
-      </widget>
-     </item>
-    </layout>
-   </item>
   </layout>
  </widget>
  <resources/>
diff --git a/src/plugins/qt4projectmanager/qt-s60/s60manager.cpp b/src/plugins/qt4projectmanager/qt-s60/s60manager.cpp
index c88c7786285..fa7b21fb573 100644
--- a/src/plugins/qt4projectmanager/qt-s60/s60manager.cpp
+++ b/src/plugins/qt4projectmanager/qt-s60/s60manager.cpp
@@ -101,14 +101,13 @@ private:
 S60Manager *S60Manager::instance() { return m_instance; }
 
 S60Manager::S60Manager(QObject *parent)
-        : QObject(parent),
-        m_devices(new S60Devices(this))
+    : QObject(parent), m_devices(S60Devices::createS60Devices(this))
 {
     m_instance = this;
+
 #ifdef QTCREATOR_WITH_S60
     addAutoReleasedObject(new S60DevicesPreferencePane(m_devices, this));
 #endif
-    m_devices->detectQtForDevices(); // Order!
 
     addAutoReleasedObject(new S60EmulatorRunConfigurationFactory);
     addAutoReleasedObject(new RunControlFactory<S60EmulatorRunControl,
-- 
GitLab