From 9185893037cfaab80b2e031df6646ee67dff77f8 Mon Sep 17 00:00:00 2001
From: Tomatix <post@trollskap.no>
Date: Tue, 15 Mar 2022 20:01:07 +0100
Subject: [PATCH] #134 More work done on the new server config editing.
 In-table editing and icons to better see what's a network and a server. Known
 issue when adding servers to networks that are expanded in the tree view,
 need to collapse and re-expand the network to see it. Saving to JSON and
 deleting servers/networks is not done yet.

---
 CMakeLists.txt                    |   2 +-
 IConfig/AddServer.cpp             |  27 +++-
 IConfig/AddServer.h               |  11 +-
 IConfig/AddServer.ui              |  42 ++---
 IConfig/CMakeLists.txt            |   4 +-
 IConfig/IConfigServers.cpp        |  26 ++-
 IConfig/IConfigServers.h          |   2 +
 IConfig/IConfigServers.ui         |  16 +-
 IConfig/ServerItem.cpp            |  18 ++-
 IConfig/ServerItem.h              |  44 +++++-
 IConfig/ServerModel.cpp           | 253 ++++++++++++++++++++++++++----
 IConfig/ServerModel.h             |  32 +++-
 IConfig/ServerOptionsDelegate.cpp |  80 ++++++++++
 IConfig/ServerOptionsDelegate.h   |  26 +++
 Resources/Icons/network.png       | Bin 0 -> 3208 bytes
 Resources/resources.qrc           |   1 +
 16 files changed, 512 insertions(+), 72 deletions(-)
 create mode 100644 IConfig/ServerOptionsDelegate.cpp
 create mode 100644 IConfig/ServerOptionsDelegate.h
 create mode 100644 Resources/Icons/network.png

diff --git a/CMakeLists.txt b/CMakeLists.txt
index dc5093e..5e044a5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -9,7 +9,7 @@ set(BUILD_TYPE "packaged")
 set(VERSION_MAJOR 1)
 set(VERSION_MINOR 1)
 set(VERSION_PATCH 0)
-set(VERSION_APPEND "dev6")
+set(VERSION_APPEND "dev7")
 
 #
 # CMake build environment setup
diff --git a/IConfig/AddServer.cpp b/IConfig/AddServer.cpp
index 39620ab..b096e57 100644
--- a/IConfig/AddServer.cpp
+++ b/IConfig/AddServer.cpp
@@ -1,7 +1,18 @@
+/*
+ * IdealIRC - Internet Relay Chat client
+ * Copyright (C) 2022  Tom-Andre Barstad.
+ * This software is licensed under the Software Attribution License.
+ * See LICENSE for more information.
+*/
+
 #include "AddServer.h"
 #include "ui_AddServer.h"
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
 
-AddServer::AddServer(QWidget *parent) :
+AddServer::AddServer(const QStringList& networkList, QWidget *parent) :
     QDialog(parent),
     ui(new Ui::AddServer)
 {
@@ -15,6 +26,9 @@ AddServer::AddServer(QWidget *parent) :
 
     connect(ui->edName, &QLineEdit::textChanged, EnableOrDisableSave);
     connect(ui->edAddress, &QLineEdit::textChanged, EnableOrDisableSave);
+
+    for (const auto& networkName : networkList)
+        ui->edNetwork->addItem(networkName);
 }
 
 AddServer::~AddServer()
@@ -47,15 +61,24 @@ bool AddServer::isServer() const
     return ui->rdServer->isChecked();
 }
 
+int AddServer::networkIndex() const
+{
+    // 0th index is the "No network" selection, make that into a "-1" index instead... and the rest must also follow.
+    return ui->edNetwork->currentIndex() - 1;
+}
+
 void AddServer::on_btnCancel_clicked()
 {
     close();
 }
 
-
 void AddServer::on_btnSave_clicked()
 {
     emit saved();
     close();
 }
 
+void AddServer::on_rdNetwork_toggled(bool checked)
+{
+    ui->edNetwork->setEnabled(!checked);
+}
diff --git a/IConfig/AddServer.h b/IConfig/AddServer.h
index c095643..3893bff 100644
--- a/IConfig/AddServer.h
+++ b/IConfig/AddServer.h
@@ -1,3 +1,10 @@
+/*
+ * IdealIRC - Internet Relay Chat client
+ * Copyright (C) 2022  Tom-Andre Barstad.
+ * This software is licensed under the Software Attribution License.
+ * See LICENSE for more information.
+*/
+
 #ifndef ADDSERVER_H
 #define ADDSERVER_H
 
@@ -12,7 +19,7 @@ class AddServer : public QDialog
     Q_OBJECT
 
 public:
-    explicit AddServer(QWidget *parent = nullptr);
+    explicit AddServer(const QStringList& networkList, QWidget *parent = nullptr);
     ~AddServer();
 
     QString name() const;
@@ -20,6 +27,7 @@ public:
     QString password() const;
     bool ssl() const;
     bool isServer() const;
+    int networkIndex() const;
 
 signals:
     void saved();
@@ -27,6 +35,7 @@ signals:
 private slots:
     void on_btnCancel_clicked();
     void on_btnSave_clicked();
+    void on_rdNetwork_toggled(bool checked);
 
 private:
     Ui::AddServer *ui;
diff --git a/IConfig/AddServer.ui b/IConfig/AddServer.ui
index a0d74a3..1e79c80 100644
--- a/IConfig/AddServer.ui
+++ b/IConfig/AddServer.ui
@@ -7,7 +7,7 @@
     <x>0</x>
     <y>0</y>
     <width>365</width>
-    <height>160</height>
+    <height>162</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -21,34 +21,39 @@
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout">
      <item>
-      <widget class="QRadioButton" name="rdServer">
+      <widget class="QRadioButton" name="rdNetwork">
        <property name="text">
-        <string>Server</string>
-       </property>
-       <property name="checked">
-        <bool>true</bool>
+        <string>Network</string>
        </property>
       </widget>
      </item>
      <item>
-      <widget class="QRadioButton" name="rdNetwork">
+      <widget class="QRadioButton" name="rdServer">
        <property name="text">
-        <string>Network</string>
+        <string>Server</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
        </property>
       </widget>
      </item>
      <item>
-      <spacer name="horizontalSpacer_2">
-       <property name="orientation">
-        <enum>Qt::Horizontal</enum>
+      <widget class="QComboBox" name="edNetwork">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
        </property>
-       <property name="sizeHint" stdset="0">
-        <size>
-         <width>40</width>
-         <height>20</height>
-        </size>
+       <property name="placeholderText">
+        <string/>
        </property>
-      </spacer>
+       <item>
+        <property name="text">
+         <string>&lt;No network&gt;</string>
+        </property>
+       </item>
+      </widget>
      </item>
     </layout>
    </item>
@@ -138,8 +143,9 @@
   </layout>
  </widget>
  <tabstops>
-  <tabstop>rdServer</tabstop>
   <tabstop>rdNetwork</tabstop>
+  <tabstop>rdServer</tabstop>
+  <tabstop>edNetwork</tabstop>
   <tabstop>edName</tabstop>
   <tabstop>edAddress</tabstop>
   <tabstop>chkSSL</tabstop>
diff --git a/IConfig/CMakeLists.txt b/IConfig/CMakeLists.txt
index daa6887..8d751fe 100644
--- a/IConfig/CMakeLists.txt
+++ b/IConfig/CMakeLists.txt
@@ -18,10 +18,12 @@ list(APPEND ${component}_SOURCES
     ${CMAKE_CURRENT_SOURCE_DIR}/AddServer.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/AddServer.h
     ${CMAKE_CURRENT_SOURCE_DIR}/AddServer.ui
-    ${CMAKE_CURRENT_SOURCE_DIR}/ServerItem.h
     ${CMAKE_CURRENT_SOURCE_DIR}/ServerItem.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/ServerItem.h
     ${CMAKE_CURRENT_SOURCE_DIR}/ServerModel.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/ServerModel.h
+    ${CMAKE_CURRENT_SOURCE_DIR}/ServerOptionsDelegate.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/ServerOptionsDelegate.h
 )
 
 add_library(${component} STATIC ${${component}_SOURCES})
diff --git a/IConfig/IConfigServers.cpp b/IConfig/IConfigServers.cpp
index 45ec7d5..0e5b7c0 100644
--- a/IConfig/IConfigServers.cpp
+++ b/IConfig/IConfigServers.cpp
@@ -36,6 +36,21 @@ IConfigServers::IConfigServers(QWidget *parent) :
     ui->chkSSL->setChecked(cf_SSL);
 
     ui->servers->setModel(&smodel);
+    ui->servers->setItemDelegateForColumn(2, &m_serverOptionsDelegate);
+    ui->servers->setItemDelegateForColumn(3, &m_serverOptionsDelegate);
+
+    for (const auto& index : smodel.getEditorColumns()) {
+        ui->servers->openPersistentEditor(index);
+    }
+
+    connect(&smodel, &ServerModel::newEntry,
+        [this](const QModelIndex& sslIdx, const QModelIndex& passwordIdx) {
+            ui->servers->openPersistentEditor(sslIdx);
+            ui->servers->openPersistentEditor(passwordIdx);
+        });
+
+    ui->servers->setColumnWidth(2, 50); // SSL column
+    ui->servers->setColumnWidth(3, 100); // Password column
 }
 
 IConfigServers::~IConfigServers()
@@ -112,17 +127,20 @@ void IConfigServers::on_servers_clicked(const QModelIndex& index)
 void IConfigServers::on_btnAddServer_clicked()
 {
     if (!addServerDlg) {
-        addServerDlg = new AddServer(this);
+        addServerDlg = new AddServer(smodel.getNetworks(), this);
         addServerDlg->show();
 
         connect(addServerDlg, &QDialog::destroyed,
-            [this](){
+            [this] {
                 addServerDlg = nullptr;
             });
 
         connect(addServerDlg, &AddServer::saved,
-            [this](){
-
+            [this] {
+                if (addServerDlg->isServer())
+                    smodel.addServer(addServerDlg->name(), addServerDlg->address(), addServerDlg->password(), addServerDlg->ssl(), addServerDlg->networkIndex());
+                else
+                    smodel.addNetwork(addServerDlg->name(), addServerDlg->address(), addServerDlg->password(), addServerDlg->ssl());
             });
     }
 }
diff --git a/IConfig/IConfigServers.h b/IConfig/IConfigServers.h
index 1f6943f..0c1ea47 100644
--- a/IConfig/IConfigServers.h
+++ b/IConfig/IConfigServers.h
@@ -9,6 +9,7 @@
 #define ICONFIGSERVERS_H
 
 #include "ServerModel.h"
+#include "ServerOptionsDelegate.h"
 #include <QWidget>
 
 namespace Ui {
@@ -39,6 +40,7 @@ private slots:
 private:
     Ui::IConfigServers *ui;
     AddServer* addServerDlg{};
+    ServerOptionsDelegate m_serverOptionsDelegate;
     ServerModel smodel;
     QString cf_Realname;
     QString cf_Username;
diff --git a/IConfig/IConfigServers.ui b/IConfig/IConfigServers.ui
index 4d19c7e..e717d03 100644
--- a/IConfig/IConfigServers.ui
+++ b/IConfig/IConfigServers.ui
@@ -169,8 +169,20 @@
    <item>
     <widget class="QTreeView" name="servers">
      <property name="editTriggers">
-      <set>QAbstractItemView::NoEditTriggers</set>
+      <set>QAbstractItemView::DoubleClicked</set>
      </property>
+     <property name="uniformRowHeights">
+      <bool>true</bool>
+     </property>
+     <property name="animated">
+      <bool>true</bool>
+     </property>
+     <attribute name="headerDefaultSectionSize">
+      <number>150</number>
+     </attribute>
+     <attribute name="headerStretchLastSection">
+      <bool>false</bool>
+     </attribute>
     </widget>
    </item>
    <item>
@@ -178,7 +190,7 @@
      <item>
       <widget class="QPushButton" name="btnAddServer">
        <property name="text">
-        <string>Add server...</string>
+        <string>Add...</string>
        </property>
       </widget>
      </item>
diff --git a/IConfig/ServerItem.cpp b/IConfig/ServerItem.cpp
index ef921c8..433ba87 100644
--- a/IConfig/ServerItem.cpp
+++ b/IConfig/ServerItem.cpp
@@ -1,17 +1,19 @@
+/*
+ * IdealIRC - Internet Relay Chat client
+ * Copyright (C) 2022  Tom-Andre Barstad.
+ * This software is licensed under the Software Attribution License.
+ * See LICENSE for more information.
+*/
+
 #include "ServerItem.h"
 
-namespace Key {
-constexpr auto Name     = "Name";
-constexpr auto Host     = "Host";
-constexpr auto Port     = "Port";
-constexpr auto Password = "Password";
-constexpr auto SSL      = "SSL";
-}
+namespace Key = ServerJsonKey;
 
-ServerItem::ServerItem(const QJsonObject& data, int row, ServerItem* parentItem)
+ServerItem::ServerItem(const QJsonObject& data, int row, ServerItem* parentItem, bool isNetwork)
     : m_data(data)
     , m_row(row)
     , m_parent(parentItem)
+    , m_isNetwork{ isNetwork }
 {}
 
 QString ServerItem::name() const
diff --git a/IConfig/ServerItem.h b/IConfig/ServerItem.h
index 7284c7d..f239079 100644
--- a/IConfig/ServerItem.h
+++ b/IConfig/ServerItem.h
@@ -1,3 +1,10 @@
+/*
+ * IdealIRC - Internet Relay Chat client
+ * Copyright (C) 2022  Tom-Andre Barstad.
+ * This software is licensed under the Software Attribution License.
+ * See LICENSE for more information.
+*/
+
 #ifndef SERVERITEM_H
 #define SERVERITEM_H
 
@@ -5,25 +12,60 @@
 #include <QString>
 #include <QJsonObject>
 
+namespace ServerJsonKey
+{
+constexpr auto Servers  = "Servers";
+constexpr auto Networks = "Networks";
+constexpr auto Name     = "Name";
+constexpr auto Host     = "Host";
+constexpr auto Port     = "Port";
+constexpr auto Password = "Password";
+constexpr auto SSL      = "SSL";
+}
+
 class ServerItem
 {
 public:
-    ServerItem(const QJsonObject& data, int row, ServerItem* parentItem);
+    ServerItem(const QJsonObject& data, int row, ServerItem* parentItem, bool isNetwork);
 
     QString name() const;
+    void setName(const QString& name) { m_data[ServerJsonKey::Name] = name; }
+
     QString address() const;
+    void setAddress(const QString& address)
+    {
+        auto hostport = address.split(':');
+
+        if (hostport.size() == 1)
+            hostport << (ssl() ? "6697" : "6667");
+
+        if (hostport[0].isEmpty())
+            hostport[0] = "server.name";
+
+        m_data[ServerJsonKey::Host] = hostport[0];
+        m_data[ServerJsonKey::Port] = hostport[1].toUShort();
+    }
+
     QString password() const;
+    void setPassword(const QString& password) { m_data[ServerJsonKey::Password] = password; }
+
     bool ssl() const;
+    void setSsl(bool enable) { m_data[ServerJsonKey::SSL] = enable; }
 
     ServerItem* parent() const { return m_parent; }
     int row() const { return m_row; }
+    void incRow() { ++m_row; }
+
     QList<ServerItem>& children() { return m_children; }
 
+    bool isNetwork() const { return m_isNetwork; }
+
 private:
     QJsonObject m_data;
     QList<ServerItem> m_children;
     int m_row;
     ServerItem* m_parent;
+    bool m_isNetwork;
 };
 
 #endif // SERVERITEM_H
diff --git a/IConfig/ServerModel.cpp b/IConfig/ServerModel.cpp
index 121d852..3403350 100644
--- a/IConfig/ServerModel.cpp
+++ b/IConfig/ServerModel.cpp
@@ -8,9 +8,31 @@
 #include "ServerModel.h"
 #include "config.h"
 #include <QJsonArray>
-#include <QJsonValue>
 #include <QFile>
 #include <QDebug>
+#include <QTimer>
+#include <QIcon>
+
+namespace {
+QJsonObject createJsonObject(const QString& name, const QString& address, const QString& password, bool isSsl)
+{
+    namespace Key = ServerJsonKey;
+
+    const auto hostport{ address.split(':') };
+    const auto host{ hostport[0] };
+    const auto port{ hostport.count() > 1 ? hostport[1].toShort() : 6667 };
+
+    QJsonObject obj;
+
+    obj[Key::Name]     = name;
+    obj[Key::Host]     = host;
+    obj[Key::Port]     = port;
+    obj[Key::SSL]      = isSsl;
+    obj[Key::Password] = password;
+
+    return obj;
+}
+}
 
 ServerModel::ServerModel(QObject* parent) :
     QAbstractItemModel(parent)
@@ -36,50 +58,58 @@ ServerModel::ServerModel(QObject* parent) :
 
 }
 
-void ServerModel::createItems(const QJsonObject& json)
+void ServerModel::saveToJsonFile(const QString& fileName) const
 {
-    constexpr auto Servers  = "Servers";
-    constexpr auto Networks = "Networks";
 
-    const auto servers = json[Servers].toArray();
-    const auto networks = json[Networks].toArray();
+}
 
-    int rootRow{ 0 };
+Qt::ItemFlags ServerModel::flags(const QModelIndex& index) const
+{
+    /* Don't care about invalid indexes */
+    if (!index.isValid())
+        return Qt::NoItemFlags;
 
-    for (const auto& server : servers) {
-        m_items << ServerItem(server.toObject(), rootRow++, nullptr);
-    }
+    /* Columns containing widgets for editing */
+    if (index.column() > 1)
+        return Qt::ItemIsEnabled | Qt::ItemIsEditable;
 
-    for (const auto& network : networks) {
-        const auto networkObj = network.toObject();
-        const auto childServers = networkObj[Servers].toArray();
+    /* Directly editable columns */
+    Qt::ItemFlags flags{ Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled };
 
-        m_items << ServerItem(networkObj, rootRow++, nullptr);
-        auto& networkItem = m_items.back();
+    const auto* item = static_cast<const ServerItem*>(index.internalPointer());
+    if (!item->isNetwork())
+        flags |= Qt::ItemNeverHasChildren;
 
-        int childRow{ 0 };
-        for (const auto& cs : childServers) {
-            auto& networkServers = networkItem.children();
-            networkServers << ServerItem(cs.toObject(), childRow++, &networkItem);
-        }
-    }
+    return flags;
 }
 
 QVariant ServerModel::data(const QModelIndex& index, int role) const
 {
-    if (!index.isValid() || role != Qt::DisplayRole)
+    if (!index.isValid())
         return {};
 
-    auto* item = static_cast<const ServerItem*>(index.internalPointer());
+    if (role == Qt::DisplayRole || role == Qt::EditRole) {
+        const auto* item = static_cast<const ServerItem*>(index.internalPointer());
 
-    if (index.column() == 0)
-        return item->name();
+        if (index.column() == 0)
+            return item->name();
 
-    else if (index.column() == 1)
-        return item->address();
+        else if (index.column() == 1)
+            return item->address();
 
-    else
-        return {};
+        else
+            return {};
+    }
+
+    if (role == Qt::DecorationRole && index.column() == 0) {
+        const auto* item = static_cast<const ServerItem*>(index.internalPointer());
+        if (item->isNetwork())
+            return QIcon(":/Icons/network.png");
+        else
+            return QIcon(":/Icons/serverwindow.png");
+    }
+
+    return {};
 }
 
 QVariant ServerModel::headerData(int section, Qt::Orientation orientation, int role) const
@@ -87,7 +117,9 @@ QVariant ServerModel::headerData(int section, Qt::Orientation orientation, int r
     if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
         QStringList labels {
             tr("Name"),
-            tr("Address")
+            tr("Address"),
+            "",
+            ""
         };
 
         return labels[section];
@@ -97,6 +129,36 @@ QVariant ServerModel::headerData(int section, Qt::Orientation orientation, int r
     }
 }
 
+bool ServerModel::setData(const QModelIndex& index, const QVariant& value, int role)
+{
+    if (!index.isValid() || role != Qt::EditRole)
+        return false;
+
+    auto* item = static_cast<ServerItem*>(index.internalPointer());
+
+    bool ret{ true };
+    switch (index.column()) {
+        case 0:
+            if (value.toString().isEmpty())
+                ret = false;
+            else
+                item->setName(value.toString());
+            break;
+
+        case 1:
+            if (value.toString().isEmpty())
+                ret = false;
+            else
+                item->setAddress(value.toString());
+            break;
+
+        default:
+            ret = false;
+    }
+
+    return ret;
+}
+
 int ServerModel::rowCount(const QModelIndex& parent) const
 {
     if (!parent.isValid())
@@ -135,3 +197,134 @@ QModelIndex ServerModel::parent(const QModelIndex& index) const
 
     return createIndex(parentItem->row(), 0, parentItem);
 }
+
+void ServerModel::addServer(const QString& name, const QString& address, const QString& password, bool isSsl, int networkIdx)
+{
+    QList<ServerItem>* items{};
+    ServerItem* parentNetworkItem{};
+    int pos{};
+
+    QModelIndex modelIdx;
+
+    /* Network server */
+    if (networkIdx > -1) {
+        modelIdx = networkIndex(networkIdx);
+        parentNetworkItem = static_cast<ServerItem*>(modelIdx.internalPointer());
+        items = &(parentNetworkItem->children());
+        pos = items->count();
+    }
+
+    /* Non-network server */
+    else {
+        items = &m_items;
+
+        for (auto& item : m_items) {
+            if (item.isNetwork())
+                item.incRow(); // Increment row number for each network, since any non-network server is inserted before these.
+            else
+                ++pos; // Finds the end position of non-network servers. Add server there (bottom of non-networks; before networks are listed.)
+        }
+    }
+
+    // TODO this fails to update the tree with new child item (network server)
+    const auto obj{ createJsonObject(name, address, password, isSsl) };
+    beginInsertRows(modelIdx, pos, pos);
+    items->insert(pos, ServerItem(obj, pos, parentNetworkItem, false));
+    endInsertRows();
+
+    emit newEntry(
+            createIndex(pos, 2, &((*items)[pos])), // SSL
+            createIndex(pos, 3, &((*items)[pos]))  // Password
+         );
+}
+
+void ServerModel::addNetwork(const QString& name, const QString& address, const QString& password, bool isSsl)
+{
+    const auto obj{ createJsonObject(name, address, password, isSsl) };
+
+    const auto idx{ m_items.count() };
+    beginInsertRows(QModelIndex{}, idx, idx);
+    m_items << ServerItem(obj, idx, nullptr, true);
+    endInsertRows();
+
+    emit newEntry(
+            createIndex(idx, 2, &(m_items.back())), // SSL
+            createIndex(idx, 3, &(m_items.back()))  // Password
+         );
+}
+
+QStringList ServerModel::getNetworks() const
+{
+    QStringList networks;
+
+    for (const auto& item : m_items) {
+        if (item.isNetwork())
+            networks << item.name();
+    }
+
+    return networks;
+}
+
+QList<QModelIndex> ServerModel::getEditorColumns()
+{
+    QList<QModelIndex> indexList;
+
+    for (auto& topItem : m_items) {
+        indexList << createIndex(topItem.row(), 2, &topItem);
+        indexList << createIndex(topItem.row(), 3, &topItem);
+
+        if (topItem.isNetwork()) {
+            auto& items = topItem.children();
+            for (auto& item : items) {
+                indexList << createIndex(item.row(), 2, &item);
+                indexList << createIndex(item.row(), 3, &item);
+            }
+        }
+    }
+
+    return indexList;
+}
+
+QModelIndex ServerModel::networkIndex(int row, int col)
+{
+    int rc{ 0 };
+    for (auto& item : m_items) {
+        if (!item.isNetwork())
+            continue;
+
+        if (rc == row)
+            return createIndex(row, col, &item);
+        else
+            ++rc;
+    }
+
+    return {};
+}
+
+void ServerModel::createItems(const QJsonObject& json)
+{
+    namespace Key = ServerJsonKey;
+
+    const auto servers = json[Key::Servers].toArray();
+    const auto networks = json[Key::Networks].toArray();
+
+    int rootRow{ 0 };
+
+    for (const auto& server : servers) {
+        m_items << ServerItem(server.toObject(), rootRow++, nullptr, false);
+    }
+
+    for (const auto& network : networks) {
+        const auto networkObj = network.toObject();
+        const auto childServers = networkObj[Key::Servers].toArray();
+
+        m_items << ServerItem(networkObj, rootRow++, nullptr, true);
+        auto& networkItem = m_items.back();
+
+        int childRow{ 0 };
+        for (const auto& cs : childServers) {
+            auto& networkServers = networkItem.children();
+            networkServers << ServerItem(cs.toObject(), childRow++, &networkItem, false);
+        }
+    }
+}
diff --git a/IConfig/ServerModel.h b/IConfig/ServerModel.h
index 6045d4e..cbb9078 100644
--- a/IConfig/ServerModel.h
+++ b/IConfig/ServerModel.h
@@ -16,7 +16,7 @@
 #include <QJsonObject>
 
 /**
- * @brief Model of servers.ini
+ * @brief Model of servers.json
 */
 class ServerModel : public QAbstractItemModel
 {
@@ -25,20 +25,44 @@ class ServerModel : public QAbstractItemModel
 public:
     explicit ServerModel(QObject* parent = nullptr);
 
+    /// Save model to a json file
+    void saveToJsonFile(const QString& fileName) const;
+
+    Qt::ItemFlags flags(const QModelIndex &index) const;
     QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
     QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
 
-    int rowCount(const QModelIndex& parent = QModelIndex()) const override;
-    int columnCount(const QModelIndex& parent = QModelIndex()) const override
+    bool setData(const QModelIndex &index, const QVariant& value, int role = Qt::EditRole) override;
+
+    int rowCount(const QModelIndex& parent = {}) const override;
+    int columnCount(const QModelIndex& parent = {}) const override
     {
-        return 2;
+        return 4;
     };
 
     QModelIndex index(int row, int column, const QModelIndex& parent = {}) const override;
     QModelIndex parent(const QModelIndex& index) const override;
 
+    /// Add a server to the model. networkIdx = -1 means server is not part of a network.
+    void addServer(const QString& name, const QString& address, const QString& password, bool isSsl, int networkIdx);
+
+    /// Add a network to the model.
+    void addNetwork(const QString& name, const QString& address, const QString& password, bool isSsl);
+
+    QStringList getNetworks() const;
+
+    QList<QModelIndex> getEditorColumns();
+
+signals:
+    void newEntry(const QModelIndex& sslIdx, const QModelIndex& passwordIdx);
+
 private:
+    QModelIndex networkIndex(int row, int col = 0);
     void createItems(const QJsonObject& json);
+
+    // I don't like this being mutable, but the nature of Qt's item model seems to have forced my hand.
+    // Or I've probably not done the correct implementation design. In any way, it works, but smells a bit...
+    // This is the top-level items as shown in the UI, same order.
     mutable QList<ServerItem> m_items;
 };
 
diff --git a/IConfig/ServerOptionsDelegate.cpp b/IConfig/ServerOptionsDelegate.cpp
new file mode 100644
index 0000000..b86b681
--- /dev/null
+++ b/IConfig/ServerOptionsDelegate.cpp
@@ -0,0 +1,80 @@
+/*
+ * IdealIRC - Internet Relay Chat client
+ * Copyright (C) 2022  Tom-Andre Barstad.
+ * This software is licensed under the Software Attribution License.
+ * See LICENSE for more information.
+*/
+
+#include "ServerOptionsDelegate.h"
+#include "ServerItem.h"
+#include <QInputDialog>
+#include <QPushButton>
+#include <QDebug>
+
+ServerOptionsDelegate::ServerOptionsDelegate(QObject* parent)
+    : QStyledItemDelegate(parent)
+{}
+
+QWidget* ServerOptionsDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
+{
+    if (!index.isValid())
+        return nullptr;
+
+    if (index.column() == 2) {
+        auto* btn = new QPushButton(tr("SSL"), parent);
+        btn->setCheckable(true);
+        return btn;
+    }
+    else if (index.column() == 3) {
+        auto* btn = new QPushButton(tr("Password"), parent);
+        auto* item = static_cast<ServerItem*>(index.internalPointer());
+        connect(btn, &QPushButton::pressed,
+            [item, parent] {
+                auto password = QInputDialog::getText(
+                    parent,
+                    tr("Enter password"),
+                    tr("New password for %1:").arg(item->name()),
+                    QLineEdit::Password
+                );
+
+                item->setPassword(password);
+            });
+
+        return btn;
+    }
+    else {
+        return nullptr;
+    }
+}
+
+void ServerOptionsDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
+{
+    const auto* item = static_cast<const ServerItem*>(index.internalPointer());
+
+    if (index.column() == 2) {
+        auto* btn = qobject_cast<QPushButton*>(editor);
+        if (btn)
+            btn->setChecked(item->ssl());
+        else
+            qCritical() << "SSL button is not a QPushButton?!";
+    }
+
+}
+
+void ServerOptionsDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
+{
+    auto* item = static_cast<ServerItem*>(index.internalPointer());
+
+    if (index.column() == 2) {
+        auto* btn = qobject_cast<QPushButton*>(editor);
+        if (btn)
+            item->setSsl(btn->isChecked());
+        else
+            qCritical() << "SSL button is not a QPushButton?!";
+    }
+}
+
+void ServerOptionsDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& /*index*/) const
+{
+    editor->setGeometry(option.rect);
+}
diff --git a/IConfig/ServerOptionsDelegate.h b/IConfig/ServerOptionsDelegate.h
new file mode 100644
index 0000000..231270f
--- /dev/null
+++ b/IConfig/ServerOptionsDelegate.h
@@ -0,0 +1,26 @@
+/*
+ * IdealIRC - Internet Relay Chat client
+ * Copyright (C) 2022  Tom-Andre Barstad.
+ * This software is licensed under the Software Attribution License.
+ * See LICENSE for more information.
+*/
+
+#ifndef SERVEROPTIONSDELEGATE_H
+#define SERVEROPTIONSDELEGATE_H
+
+#include <QStyledItemDelegate>
+
+class ServerOptionsDelegate : public QStyledItemDelegate
+{
+    Q_OBJECT
+
+public:
+    explicit ServerOptionsDelegate(QObject* parent = nullptr);
+
+    QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
+    void setEditorData(QWidget* editor, const QModelIndex& index) const override;
+    void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override;
+    void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
+};
+
+#endif // SERVEROPTIONSDELEGATE_H
diff --git a/Resources/Icons/network.png b/Resources/Icons/network.png
new file mode 100644
index 0000000000000000000000000000000000000000..6761a6f7361205d6a0b81e55f0e514f2ba019336
GIT binary patch
literal 3208
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

literal 0
HcmV?d00001

diff --git a/Resources/resources.qrc b/Resources/resources.qrc
index 94b2cc1..88f94b4 100644
--- a/Resources/resources.qrc
+++ b/Resources/resources.qrc
@@ -14,5 +14,6 @@
         <file>Icons/serverwindow.png</file>
         <file>Icons/connect.png</file>
         <file>Icons/disconnect.png</file>
+        <file>Icons/network.png</file>
     </qresource>
 </RCC>