[plasma/plasma-nm] /: Port the IPv4 configuration page to QML

Devin Lin <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 3ab67543c97cd473d4c5ffee026177c708fc5a8d by Devin Lin, on behalf of Tushar Gupta.
Committed on 16/08/2026 at 03:50.
Pushed by devinlin into branch 'master'.

Port the IPv4 configuration page to QML

## Backend

### `settings/ipv6settings/ipv6settings.{h,cpp}`

Introduces `IPv6Settings`, exposed to QML as `IPv6Setting`. This class owns the entire `ipv6` configuration section as QML properties. Configuration is loaded via `loadConfig(NetworkManager::Ipv6Setting::Ptr)` and serialized back to NetworkManager through `setting()`, which returns a `QVariantMap`.

The IPv6 method (Automatic, Automatic (only addresses), Link-Local, Manual, Ignored, or Disabled) drives several derived properties, including `dnsEnabled`, `addressTableEnabled`, `routesEnabled`, and `dnsLabel`.

The IPv6-specific address and prefix validation is handled by the settings class, including validation of IPv6 addresses, prefix lengths, and gateways before the configuration is serialized.

### `settings/ipv6settings/routetablemodel.{h,cpp}`

Adds `RouteTableModel`, a `QAbstractTableModel` representing a list of `Ipv6RouteEntry` objects (destination address, prefix length, next hop, and metric).

A proper `QAbstractItemModel` is required instead of a simple list property because `TableView` only supports multi-column layouts and injects the required `row` and `column` properties when backed by an item model.

The address table reuses this same model rather than introducing a nearly identical second implementation.

## QML

* **`IPv6Settings.qml`** – Implements the main IPv6 configuration page, including the method selector, DNS servers, search domains, address table, route metric, the "IPv6 is required for this connection" checkbox, and the **Advanced…** and **Routes…** buttons.
* **`IPv6Routes.qml`** – Implements the routes dialog using `TableView` with `HorizontalHeaderView` and `VerticalHeaderView`. Row addition and removal are driven by an `ItemSelectionModel`, alongside options for "Ignore automatically obtained routes" and "Use only for resources on this connection".

Both dialogs edit the shared `IPv6Settings` instance directly. To preserve cancel semantics, each dialog snapshots the properties it modifies in `onOpened` and restores them in `onRejected`.

## KCM Integration

`kcm.{h,cpp}` now exposes an `ipv6Settings` property, invokes `loadConfig()` when either loading an existing connection or creating a new one, inserts the generated `ipv6` map during `save()`, and connects `IPv6Settings::validChanged` to the KCM's dirty state so the **Apply** button is enabled whenever IPv6 settings change.

M  +25   -27   kcms/kcm_connections_qml/kcm.cpp
M  +4    -1    kcms/kcm_connections_qml/kcm.h
M  +10   -0    libs/editorqml/CMakeLists.txt
M  +6    -1    libs/editorqml/qml/components/Wireless.qml
A  +97   -0    libs/editorqml/qml/ipv4/IPv4Advance.qml     [License: LGPL(3+eV) LGPL(v3.0) LGPL(v2.1)]
A  +219  -0    libs/editorqml/qml/ipv4/IPv4Routes.qml     [License: LGPL(3+eV) LGPL(v3.0) LGPL(v2.1)]
A  +362  -0    libs/editorqml/qml/ipv4/IPv4Settings.qml     [License: LGPL(3+eV) LGPL(v3.0) LGPL(v2.1)]
C  +4    -3    libs/editorqml/qml/ipv4/SearchDomainList.qml [from: libs/editorqml/qml/wifisecurity/DnsList.qml - 094% similarity]
M  +0    -2    libs/editorqml/qml/wificonnectionsettings/WifiConnectionSettings.qml
M  +4    -2    libs/editorqml/qml/wifisecurity/DnsList.qml
M  +0    -2    libs/editorqml/settings/generalSettings/generalsettings.h
A  +166  -0    libs/editorqml/settings/ipv4settings/ipv4routetablemodel.cpp     [License: LGPL(3+eV) LGPL(v3.0) LGPL(v2.1)]
A  +71   -0    libs/editorqml/settings/ipv4settings/ipv4routetablemodel.h     [License: LGPL(3+eV) LGPL(v3.0) LGPL(v2.1)]
A  +478  -0    libs/editorqml/settings/ipv4settings/ipv4settings.cpp     [License: LGPL(3+eV) LGPL(v3.0) LGPL(v2.1)]
A  +156  -0    libs/editorqml/settings/ipv4settings/ipv4settings.h     [License: LGPL(3+eV) LGPL(v3.0) LGPL(v2.1)]

https://invent.kde.org/plasma/plasma-nm/-/commit/3ab67543c97cd473d4c5ffee026177c708fc5a8d

diff --git a/kcms/kcm_connections_qml/kcm.cpp b/kcms/kcm_connections_qml/kcm.cpp
index e9ef23153..22fd5e536 100644
--- a/kcms/kcm_connections_qml/kcm.cpp
+++ b/kcms/kcm_connections_qml/kcm.cpp
@@ -35,6 +35,7 @@ KCMNetworkManagementQml::KCMNetworkManagementQml(QObject *parent, const KPluginM
     , m_connectionStatus(new ConnectionStatus(this))
     , m_generalSettings(new GeneralSetting(this))
     , m_wifiSetting(new WifiSetting(this))
+    , m_ipv4Settings(new IPv4Settings(this))
     , m_timer(new QTimer(this))
 {
     // Check if we can use AP mode to identify security type
@@ -128,7 +129,15 @@ KCMNetworkManagementQml::KCMNetworkManagementQml(QObject *parent, const KPluginM
     });
 
     connect(m_wifiSetting, &WifiSetting::validChanged, this, [this]() {
-        kcmChanged(true);
+        if (m_wifiSetting->isValid()) {
+            kcmChanged(true);
+        }
+    });
+
+    connect(m_ipv4Settings, &IPv4Settings::validChanged, this, [this]() {
+        if (m_ipv4Settings->isValid()) {
+            kcmChanged(true);
+        }
     });
 
     connect(NetworkManager::settingsNotifier(),
@@ -148,6 +157,11 @@ KCMNetworkManagementQml::KCMNetworkManagementQml(QObject *parent, const KPluginM
 
 KCMNetworkManagementQml::~KCMNetworkManagementQml() = default;
 
+IPv4Settings *KCMNetworkManagementQml::ipv4Settings() const
+{
+    return m_ipv4Settings;
+}
+
 GeneralSetting *KCMNetworkManagementQml::generalSettings() const
 {
     return m_generalSettings;
@@ -216,25 +230,6 @@ Enums::ConnectionType KCMNetworkManagementQml::connectionType() const
     }
 }
 
-bool KCMNetworkManagementQml::isValid() const
-{
-    if (m_connectionType == NetworkManager::ConnectionSettings::Wireless) {
-        if (!m_wifiSetting->isValid()) {
-            return false;
-        }
-
-        if (!m_wifiSecurity->isValid()) {
-            return false;
-        }
-
-        if (m_wifiSecurity->enabled8021x() && !m_security8021xSetting->isValid()) {
-            return false;
-        }
-    }
-
-    return true;
-}
-
 void KCMNetworkManagementQml::load()
 {
     if (m_currentConnectionPath.isEmpty()) {
@@ -281,18 +276,16 @@ void KCMNetworkManagementQml::applyWirelessSetting(NMVariantMapMap &map)
             groupEncrypts << NetworkManager::WirelessSecuritySetting::Ccmp;
             securitySetting->setGroup(groupEncrypts);
         }
-    }
 
-    map = settings.toMap();
+        map[QStringLiteral("802-11-wireless")] = wirelessSetting->toMap();
+        if (map.contains(QStringLiteral("802-11-wireless-security"))) {
+            map[QStringLiteral("802-11-wireless-security")] = securitySetting->toMap();
+        }
+    }
 }
 
 void KCMNetworkManagementQml::save()
 {
-    if (!isValid()) {
-        qCWarning(PLASMA_NM_KCM_QML_LOG) << "Cannot save: connection settings are invalid";
-        return;
-    }
-
     // process the pending settings
     if (m_pendingNewSettings) {
         m_generalSettings->applyTo(m_pendingNewSettings, m_wifiSecurity);
@@ -300,6 +293,7 @@ void KCMNetworkManagementQml::save()
         if (m_pendingNewSettings->connectionType() == NetworkManager::ConnectionSettings::Wireless) {
             map.insert(QStringLiteral("802-11-wireless"), m_wifiSetting->setting());
         }
+        map.insert(QStringLiteral("ipv4"), m_ipv4Settings->setting());
         map.insert(QStringLiteral("802-11-wireless-security"), m_wifiSecurity->setting());
         if (m_wifiSecurity->enabled8021x()) {
             map.insert(QStringLiteral("802-1x"), m_wifiSecurity->setting8021x());
@@ -326,6 +320,7 @@ void KCMNetworkManagementQml::save()
     if (m_connectionType == NetworkManager::ConnectionSettings::Wireless) {
         map.insert(QStringLiteral("802-11-wireless"), m_wifiSetting->setting());
     }
+    map.insert(QStringLiteral("ipv4"), m_ipv4Settings->setting());
     if (m_wifiSecurity->securityType() != WifiSecuritySetting::None) {
         map.insert(QStringLiteral("802-11-wireless-security"), m_wifiSecurity->setting());
     } else {
@@ -368,6 +363,7 @@ void KCMNetworkManagementQml::loadConnectionSettings(const NetworkManager::Conne
     Q_EMIT connectionTypeChanged();
     m_generalSettings->loadConfig(connectionSettings);
     m_wifiSetting->loadConfig(connectionSettings);
+    m_ipv4Settings->loadConfig(connectionSettings->setting(NetworkManager::Setting::Ipv4).staticCast<NetworkManager::Ipv4Setting>());
     // check wireless only for rn
     if (connectionSettings->connectionType() != NetworkManager::ConnectionSettings::Wireless) {
         kcmChanged(false);
@@ -444,6 +440,8 @@ void KCMNetworkManagementQml::addConnection(const NetworkManager::ConnectionSett
         m_wifiSetting->loadConfig(connectionSettings);
     }
 
+    m_ipv4Settings->loadConfig(connectionSettings->setting(NetworkManager::Setting::Ipv4).staticCast<NetworkManager::Ipv4Setting>());
+
     Q_EMIT connectionLoaded(QString());
 }
 
diff --git a/kcms/kcm_connections_qml/kcm.h b/kcms/kcm_connections_qml/kcm.h
index 3adec1f59..ccf41c1b1 100644
--- a/kcms/kcm_connections_qml/kcm.h
+++ b/kcms/kcm_connections_qml/kcm.h
@@ -11,6 +11,7 @@
 #include "enums.h"
 #include "generalsettings.h"
 #include "handler.h"
+#include "ipv4settings.h"
 #include "security8021xsetting.h"
 #include "wifisecuritysetting.h"
 #include "wifisetting.h"
@@ -32,6 +33,7 @@ class KCMNetworkManagementQml : public KQuickConfigModule
     Q_PROPERTY(Enums::ConnectionType connectionType READ connectionType NOTIFY connectionTypeChanged)
     Q_PROPERTY(GeneralSetting *generalSettings READ generalSettings CONSTANT)
     Q_PROPERTY(WifiSetting *wifiSetting READ wifiSetting CONSTANT)
+    Q_PROPERTY(IPv4Settings *ipv4Settings READ ipv4Settings CONSTANT)
 
 public:
     explicit KCMNetworkManagementQml(QObject *parent, const KPluginMetaData &metaData);
@@ -44,6 +46,7 @@ public:
     ConnectionStatus *connectionStatus() const;
     GeneralSetting *generalSettings() const;
     WifiSetting *wifiSetting() const;
+    IPv4Settings *ipv4Settings() const;
     bool useApMode() const;
 
     Q_INVOKABLE void onRequestCreateConnection(int connectionType, const QString &vpnType, const QString &specificType, bool shared);
@@ -77,7 +80,6 @@ private:
     };
 
 private:
-    bool isValid() const;
     void applyWirelessSetting(NMVariantMapMap &map);
     void addConnection(const NetworkManager::ConnectionSettings::Ptr &connectionSettings);
     void kcmChanged(bool kcmChanged);
@@ -98,6 +100,7 @@ private:
     ConnectionStatus *const m_connectionStatus;
     GeneralSetting *const m_generalSettings;
     WifiSetting *const m_wifiSetting;
+    IPv4Settings *const m_ipv4Settings;
 
     bool m_useApMode = false;
 
diff --git a/libs/editorqml/CMakeLists.txt b/libs/editorqml/CMakeLists.txt
index 06c4e527c..6f67c3b1a 100644
--- a/libs/editorqml/CMakeLists.txt
+++ b/libs/editorqml/CMakeLists.txt
@@ -13,6 +13,10 @@ set(SETTINGS_SRCS
   settings/generalSettings/advancedpermissionsmodel.cpp
   settings/wificonnectionsettings/wifisetting.cpp
   settings/wificonnectionsettings/wifisetting.h
+  settings/ipv4settings/ipv4settings.cpp
+  settings/ipv4settings/ipv4settings.h
+  settings/ipv4settings/ipv4routetablemodel.cpp
+  settings/ipv4settings/ipv4routetablemodel.h
 )
 
 set(QML_SRCS
@@ -48,6 +52,11 @@ set(QML_SRCS
   qml/generalSettings/GeneralSettings.qml
   qml/generalSettings/AdvancedPermissionsDialog.qml
   qml/wificonnectionsettings/WifiConnectionSettings.qml
+
+  qml/ipv4/IPv4Settings.qml
+  qml/ipv4/IPv4Advance.qml
+  qml/ipv4/IPv4Routes.qml
+  qml/ipv4/SearchDomainList.qml
 )
 
 add_library(plasmanm_editorqml SHARED ${SETTINGS_SRCS})
@@ -109,6 +118,7 @@ target_include_directories(plasmanm_editorqml
         ${CMAKE_CURRENT_SOURCE_DIR}/settings/wifistatus
         ${CMAKE_CURRENT_SOURCE_DIR}/settings/generalSettings
         ${CMAKE_CURRENT_SOURCE_DIR}/settings/wificonnectionsettings
+        ${CMAKE_CURRENT_SOURCE_DIR}/settings/ipv4settings
 )
 
 target_compile_definitions(plasmanm_editorqml PRIVATE BROADBANDPROVIDER_DATABASE=\"${BROADBANDPROVIDER_DATABASE}\")
diff --git a/libs/editorqml/qml/components/Wireless.qml b/libs/editorqml/qml/components/Wireless.qml
index 424c38d12..35f0cbf8b 100644
--- a/libs/editorqml/qml/components/Wireless.qml
+++ b/libs/editorqml/qml/components/Wireless.qml
@@ -71,7 +71,12 @@ ColumnLayout {
             }
         }
 
-        Item { /* TODO: ipv4 settings page */ }
+        Item {
+            PlasmaNMQ.IPv4Settings {
+                anchors.fill: parent
+                setting: kcm.ipv4Settings
+            }
+        }
 
         Item { /* TODO: ipv6 settings page */ }
     }
diff --git a/libs/editorqml/qml/ipv4/IPv4Advance.qml b/libs/editorqml/qml/ipv4/IPv4Advance.qml
new file mode 100644
index 000000000..01cb791a2
--- /dev/null
+++ b/libs/editorqml/qml/ipv4/IPv4Advance.qml
@@ -0,0 +1,97 @@
+/*
+        SPDX-FileCopyrightText: 2026 Tushar Gupta <[email protected]>
+
+        SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+import QtQuick
+import QtQuick.Controls as QQC2
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+import org.kde.plasma.networkmanagement.editorqml
+
+Kirigami.Dialog {
+    id: dialog
+
+    required property IPv4Setting setting
+
+    title: i18n("Advanced IPv4 Settings")
+    standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
+
+    preferredWidth: Kirigami.Units.gridUnit * 26
+
+    property var previousState: null
+
+    onOpened: previousState = {
+        dhcpSendHostname: dialog.setting.dhcpSendHostname,
+        dhcpHostname: dialog.setting.dhcpHostname,
+        dadTimeout: dialog.setting.dadTimeout
+    }
+
+    onRejected: {
+        if (!previousState)
+            return;
+        dialog.setting.dhcpSendHostname = previousState.dhcpSendHostname;
+        dialog.setting.dhcpHostname = previousState.dhcpHostname;
+        dialog.setting.dadTimeout = previousState.dadTimeout;
+    }
+
+    Kirigami.FormLayout {
+        id: form
+
+        QQC2.CheckBox {
+            id: sendHostnameCheck
+
+            Kirigami.FormData.label: i18n("DHCP:")
+            text: i18n("Send hostname to DHCP server")
+
+            checked: dialog.setting.dhcpSendHostname
+            onToggled: dialog.setting.dhcpSendHostname = checked
+        }
+
+        QQC2.TextField {
+            Kirigami.FormData.label: i18n("DHCP hostname:")
+            Layout.fillWidth: true
+
+            placeholderText: dialog.setting.systemHostname
+
+            text: dialog.setting.dhcpHostname
+            onTextEdited: dialog.setting.dhcpHostname = text
+        }
+
+        QQC2.SpinBox {
+            Kirigami.FormData.label: i18n("DAD timeout:")
+            Layout.fillWidth: true
+
+            from: -1
+            to: 30000
+            stepSize: 100
+            editable: true
+
+            value: dialog.setting.dadTimeout
+            onValueModified: dialog.setting.dadTimeout = value
+
+            textFromValue: (value, locale) => {
+                if (value < 0)
+                    return i18nc("@item DAD timeout left to NetworkManager", "Default");
+                if (value === 0)
+                    return i18nc("@item duplicate address detection turned off", "Disabled");
+                return i18nc("@item:intext duplicate address detection timeout in milliseconds", "%1 ms", Number(value).toLocaleString(locale, 'f', 0));
+            }
+
+            valueFromText: (text, locale) => {
+                if (text === i18nc("@item DAD timeout left to NetworkManager", "Default"))
+                    return -1;
+                if (text === i18nc("@item duplicate address detection turned off", "Disabled"))
+                    return 0;
+                const digits = text.replace(/[^0-9]/g, "");
+                return digits.length > 0 ? parseInt(digits, 10) : -1;
+            }
+
+            hoverEnabled: true
+            QQC2.ToolTip.visible: hovered
+            QQC2.ToolTip.delay: Kirigami.Units.humanMoment
+            QQC2.ToolTip.text: i18n("Timeout in milliseconds used to check for the presence of duplicate IP addresses on the network.\n\n0 disables the check, -1 lets NetworkManager decide.")
+        }
+    }
+}
diff --git a/libs/editorqml/qml/ipv4/IPv4Routes.qml b/libs/editorqml/qml/ipv4/IPv4Routes.qml
new file mode 100644
index 000000000..140b69b13
--- /dev/null
+++ b/libs/editorqml/qml/ipv4/IPv4Routes.qml
@@ -0,0 +1,219 @@
+/*
+        SPDX-FileCopyrightText: 2026 Tushar Gupta <[email protected]>
+
+        SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+import QtQuick
+import QtQuick.Controls as QQC2
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+import org.kde.plasma.networkmanagement.editorqml
+
+Kirigami.Dialog {
+    id: dialog
+
+    required property IPv4Setting setting
+
+    title: i18n("Edit IPv4 Routes")
+    standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
+
+    preferredWidth: Kirigami.Units.gridUnit * 46
+    preferredHeight: Kirigami.Units.gridUnit * 24
+
+    property var previousState: null
+
+    onOpened: previousState = {
+        routes: dialog.setting.routes,
+        ignoreAutoRoutes: dialog.setting.ignoreAutoRoutes,
+        neverDefault: dialog.setting.neverDefault
+    }
+
+    onRejected: {
+        if (!previousState)
+            return;
+        dialog.setting.routes = previousState.routes;
+        dialog.setting.ignoreAutoRoutes = previousState.ignoreAutoRoutes;
+        dialog.setting.neverDefault = previousState.neverDefault;
+    }
+
+    ColumnLayout {
+        Layout.fillWidth: true
+        Layout.fillHeight: true
+        Layout.margins: Kirigami.Units.largeSpacing
+
+        spacing: Kirigami.Units.smallSpacing
+
+        QQC2.Frame {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            Layout.minimumHeight: Kirigami.Units.gridUnit * 12
+
+            padding: 0
+
+            GridLayout {
+                anchors.fill: parent
+
+                columns: 2
+                rowSpacing: 0
+                columnSpacing: 0
+
+                Item {
+                    Layout.preferredWidth: verticalHeader.width
+                    Layout.preferredHeight: horizontalHeader.height
+                }
+
+                QQC2.HorizontalHeaderView {
+                    id: horizontalHeader
+
+                    Layout.fillWidth: true
+
+                    syncView: routeTable
+                    clip: true
+                }
+
+                QQC2.VerticalHeaderView {
+                    id: verticalHeader
+
+                    Layout.fillHeight: true
+
+                    syncView: routeTable
+                    clip: true
+                }
+
+                TableView {
+                    id: routeTable
+
+                    Layout.fillWidth: true
+                    Layout.fillHeight: true
+
+                    clip: true
+                    rowSpacing: 1
+                    columnSpacing: 1
+
+                    model: dialog.setting.routeModel
+
+                    selectionModel: ItemSelectionModel {
+                        id: routeSelection
+
+                        model: routeTable.model
+                    }
+
+                    onWidthChanged: Qt.callLater(routeTable.forceLayout)
+
+                    columnWidthProvider: function (column) {
+                        const metric = Kirigami.Units.gridUnit * 6;
+                        if (column === RouteTableModel.MetricColumn)
+                            return metric;
+
+                        const available = routeTable.width - metric - routeTable.columnSpacing * 3;
+                        return Math.max(Kirigami.Units.gridUnit * 8, available / 3);
+                    }
+
+                    delegate: QQC2.TextField {
+                        id: cell
+
+                        required property int row
+                        required property int column
+                        required property var display
+
+                        text: cell.display === undefined ? "" : cell.display
+
+                        placeholderText: {
+                            switch (cell.column) {
+                            case RouteTableModel.AddressColumn:
+                                return i18nc("@info:placeholder", "10.0.0.0");
+                            case RouteTableModel.NetmaskColumn:
+                                return i18nc("@info:placeholder", "255.255.255.0");
+                            case RouteTableModel.NextHopColumn:
+                                return i18nc("@info:placeholder optional field", "Optional");
+                            default:
+                                return "";
+                            }
+                        }
+
+                        validator: cell.column === RouteTableModel.MetricColumn ? metricValidator : null
+
+                        onActiveFocusChanged: if (activeFocus)
+                            routeSelection.setCurrentIndex(routeTable.model.index(cell.row, cell.column), ItemSelectionModel.ClearAndSelect)
+
+                        onEditingFinished: {
+                            if (cell.column === RouteTableModel.MetricColumn) {
+                                const parsed = parseInt(text, 10);
+                                const metric = isNaN(parsed) ? 0 : Math.min(parsed, 4294967295);
+                                dialog.setting.routeModel.setRouteField(cell.row, cell.column, metric);
+                                text = Number(metric).toFixed(0);
+                            } else {
+                                dialog.setting.routeModel.setRouteField(cell.row, cell.column, text);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        RegularExpressionValidator {
+            id: metricValidator
+
+            regularExpression: /^\d{1,10}$/
+        }
+
+        RowLayout {
+            Layout.fillWidth: true
+            spacing: Kirigami.Units.smallSpacing
+
+            Item {
+                Layout.fillWidth: true
+            }
+
+            QQC2.Button {
+                text: i18nc("@action:button Insert a row", "Add")
+                icon.name: "list-add"
+
+                onClicked: dialog.setting.routeModel.addRoute()
+            }
+
+            QQC2.Button {
+                text: i18nc("@action:button Remove a selected row", "Remove")
+                icon.name: "list-remove"
+
+                enabled: routeSelection.hasSelection
+
+                onClicked: dialog.setting.routeModel.removeRoute(routeSelection.currentIndex.row)
+            }
+
+            Item {
+                Layout.fillWidth: true
+            }
+        }
+
+        QQC2.CheckBox {
+            Layout.fillWidth: true
+            Layout.topMargin: Kirigami.Units.largeSpacing
+
+            text: i18n("Ignore automatically obtained routes")
+
+            checked: dialog.setting.ignoreAutoRoutes
+            onToggled: dialog.setting.ignoreAutoRoutes = checked
+
+            hoverEnabled: true
+            QQC2.ToolTip.visible: hovered
+            QQC2.ToolTip.delay: Kirigami.Units.humanMoment
+            QQC2.ToolTip.text: i18n("If enabled, automatically configured routes are ignored and only routes specified above are used")
+        }
+
+        QQC2.CheckBox {
+            Layout.fillWidth: true
+
+            text: i18n("Use only for resources on this connection")
+
+            checked: dialog.setting.neverDefault
+            onToggled: dialog.setting.neverDefault = checked
+
+            hoverEnabled: true
+            QQC2.ToolTip.visible: hovered
+            QQC2.ToolTip.delay: Kirigami.Units.humanMoment
+            QQC2.ToolTip.text: i18n("If enabled, this connection will never be used as the default network connection")
+        }
+    }
+}
diff --git a/libs/editorqml/qml/ipv4/IPv4Settings.qml b/libs/editorqml/qml/ipv4/IPv4Settings.qml
new file mode 100644
index 000000000..38c4e3e75
--- /dev/null
+++ b/libs/editorqml/qml/ipv4/IPv4Settings.qml
@@ -0,0 +1,362 @@
+/*
+      SPDX-FileCopyrightText: 2026 Tushar Gupta <[email protected]>
+
+      SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+import QtQuick
+import QtQuick.Controls as QQC2
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+import org.kde.plasma.networkmanagement.editorqml
+
+ColumnLayout {
+    id: root
+
+    required property IPv4Setting setting
+
+    spacing: Kirigami.Units.largeSpacing
+
+    Kirigami.FormLayout {
+        id: form
+
+        Layout.fillWidth: true
+
+        QQC2.ComboBox {
+            Kirigami.FormData.label: i18n("Method:")
+            Layout.fillWidth: true
+
+            model: [i18nc("@item:inlistbox IPv4 method", "Automatic"), i18nc("@item:inlistbox IPv4 method", "Automatic (Only addresses)"), i18nc("@item:inlistbox IPv4 method", "Link-Local"), i18nc("@item:inlistbox like in use Manual configuration", "Manual"), i18nc("@item:inlistbox IPv4 method", "Shared to other computers"), i18nc("@item:inlistbox like in this setting is Disabled", "Disabled")]
+
+            currentIndex: root.setting.method
+            onActivated: root.setting.method = currentIndex
+
+            hoverEnabled: true
+            QQC2.ToolTip.visible: hovered
+            QQC2.ToolTip.delay: Kirigami.Units.humanMoment
+            QQC2.ToolTip.text: i18n("How to configure the IPv4 address:\n• Automatic: obtain address and DNS automatically via DHCP\n• Automatic (Only addresses): obtain address via DHCP but configure DNS manually\n• Link-Local: use automatic link-local addressing (169.254.x.x)\n• Manual: enter a static IP address\n• Shared: turn this computer into a router to share internet with other devices\n• Disabled: turn off IPv4")
+        }
+
+        RowLayout {
+            Kirigami.FormData.label: root.setting.dnsLabel
+            Layout.fillWidth: true
+            spacing: Kirigami.Units.smallSpacing
+
+            enabled: root.setting.dnsEnabled
+
+            QQC2.TextField {
+                id: dnsField
+
+                Layout.fillWidth: true
+                Layout.minimumWidth: Kirigami.Units.gridUnit * 14
+
+                text: root.setting.dns
+                onTextEdited: root.setting.dns = text
+
+                hoverEnabled: true
+                QQC2.ToolTip.visible: hovered
+                QQC2.ToolTip.delay: Kirigami.Units.humanMoment
+                QQC2.ToolTip.text: i18n("Use this field to specify the IP address(es) of one or more DNS servers. Use ',' to separate entries.")
+            }
+
+            QQC2.Button {
+                icon.name: "document-properties"
+
+                onClicked: {
+                    dnsDialog.addresses = dnsField.text.length > 0 ? dnsField.text.split(",").map(s => s.trim()).filter(s => s.length > 0) : [];
+                    dnsDialog.open();
+                }
+
+                hoverEnabled: true
+                QQC2.ToolTip.visible: hovered
+                QQC2.ToolTip.delay: Kirigami.Units.humanMoment
+                QQC2.ToolTip.text: i18n("Edit the list of DNS servers")
+            }
+        }
+
+        RowLayout {
+            Kirigami.FormData.label: i18nc("@label:textbox", "Search Domains:")
+            Layout.fillWidth: true
+            spacing: Kirigami.Units.smallSpacing
+
+            enabled: root.setting.dnsEnabled
+
+            QQC2.TextField {
+                id: dnsSearchField
+
+                Layout.fillWidth: true
+                Layout.minimumWidth: Kirigami.Units.gridUnit * 14
+
+                text: root.setting.dnsSearch
+                onTextEdited: root.setting.dnsSearch = text
+
+                hoverEnabled: true
+                QQC2.ToolTip.visible: hovered
+                QQC2.ToolTip.delay: Kirigami.Units.humanMoment
+                QQC2.ToolTip.text: i18n("Use this field to specify one or more DNS domains to search. Use ',' to separate entries.")
+            }
+
+            QQC2.Button {
+                icon.name: "document-properties"
+
+                onClicked: {
+                    searchDomainDialog.addresses = dnsSearchField.text.length > 0 ? dnsSearchField.text.split(",").map(s => s.trim()).filter(s => s.length > 0) : [];
+                    searchDomainDialog.open();
+                }
+
+                hoverEnabled: true
+                QQC2.ToolTip.visible: hovered
+                QQC2.ToolTip.delay: Kirigami.Units.humanMoment
+                QQC2.ToolTip.text: i18n("Edit the list of DNS domains being searched")
+            }
+        }
+
+        QQC2.TextField {
+            Kirigami.FormData.label: i18n("DHCP Client ID:")
+            Layout.fillWidth: true
+
+            enabled: root.setting.dhcpClientIdEnabled
+
+            text: root.setting.dhcpClientId
+            onTextEdited: root.setting.dhcpClientId = text
+
+            hoverEnabled: true
+            QQC2.ToolTip.visible: hovered
+            QQC2.ToolTip.delay: Kirigami.Units.humanMoment
+            QQC2.ToolTip.text: i18n("Use this field to specify the DHCP client ID which is a string sent to the DHCP server to identify the local machine that the DHCP server may use to customize the DHCP lease and options.")
+        }
+        QQC2.SpinBox {
+            Kirigami.FormData.label: i18n("Route Metric:")
+            Layout.fillWidth: true
+
+            from: -1
+            to: 2147483647
+            editable: true
+
+            value: root.setting.routeMetric
+            onValueModified: root.setting.routeMetric = value
+
+            textFromValue: (value, locale) => Number(value).toLocaleString(locale, 'f', 0)
+
+            valueFromText: (text, locale) => {
+                const digits = text.replace(/[^0-9-]/g, "");
+                const parsed = parseInt(digits, 10);
+
+                if (isNaN(parsed))
+                    return -1;
+
+                return Math.max(-1, Math.min(parsed, 2147483647));
+            }
+
+            hoverEnabled: true
+
+            QQC2.ToolTip.visible: hovered
+            QQC2.ToolTip.delay: Kirigami.Units.humanMoment
+            QQC2.ToolTip.text: i18n("A score that helps choose the best path for data packets to reach their destination, based on factors like distance, delay, and reliability.\n\nDefault value -1 means that NetworkManager gets to choose and manage the actual metric of the route.")
+        }
+    }
+
+    Kirigami.Heading {
+        Layout.fillWidth: true
+
+        text: i18n("Addresses")
+        level: 2
+        font.weight: Font.Bold
+
+        visible: root.setting.addressTableEnabled
+    }
+
+    RowLayout {
+        Layout.fillWidth: true
+        Layout.fillHeight: true
+        spacing: Kirigami.Units.largeSpacing
+
+        visible: root.setting.addressTableEnabled
+
+        QQC2.Frame {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            Layout.minimumHeight: Kirigami.Units.gridUnit * 7
+            Layout.minimumWidth: Kirigami.Units.gridUnit * 20
+
+            padding: 0
+
+            GridLayout {
+                anchors.fill: parent
+
+                columns: 2
+                rowSpacing: 0
+                columnSpacing: 0
+
+                Item {
+                    Layout.preferredWidth: addressVerticalHeader.width
+                    Layout.preferredHeight: addressHorizontalHeader.height
+                }
+
+                QQC2.HorizontalHeaderView {
+                    id: addressHorizontalHeader
+
+                    Layout.fillWidth: true
+
+                    syncView: addressTable
+                    clip: true
+                }
+
+                QQC2.VerticalHeaderView {
+                    id: addressVerticalHeader
+
+                    Layout.fillHeight: true
+
+                    syncView: addressTable
+                    clip: true
+                }
+
+                TableView {
+                    id: addressTable
+
+                    Layout.fillWidth: true
+                    Layout.fillHeight: true
+
+                    clip: true
+                    rowSpacing: 1
+                    columnSpacing: 1
+
+                    model: root.setting.addressModel
+
+                    selectionModel: ItemSelectionModel {
+                        id: addressSelection
+
+                        model: addressTable.model
+                    }
+
+                    onWidthChanged: Qt.callLater(addressTable.forceLayout)
+
+                    columnWidthProvider: function (column) {
+                        if (column === RouteTableModel.MetricColumn)
+                            return 0;
+
+                        const available = addressTable.width - addressTable.columnSpacing * 2;
+                        return Math.max(Kirigami.Units.gridUnit * 6, available / 3);
+                    }
+
+                    delegate: QQC2.TextField {
+                        id: addressCell
+
+                        required property int row
+                        required property int column
+                        required property var display
+
+                        text: addressCell.display === undefined ? "" : addressCell.display
+
+                        placeholderText: {
+                            switch (addressCell.column) {
+                            case RouteTableModel.AddressColumn:
+                                return i18nc("@info:placeholder", "10.0.0.0");
+                            case RouteTableModel.NetmaskColumn:
+                                return i18nc("@info:placeholder", "255.255.255.0");
+                            default:
+                                return i18nc("@info:placeholder optional field", "Optional");
+                            }
+                        }
+
+                        onActiveFocusChanged: if (activeFocus)
+                            addressSelection.setCurrentIndex(addressTable.model.index(addressCell.row, addressCell.column), ItemSelectionModel.ClearAndSelect)
+
+                        onEditingFinished: {
+                            root.setting.addressModel.setRouteField(addressCell.row, addressCell.column, text);
+                            if (addressCell.column === RouteTableModel.AddressColumn)
+                                root.setting.suggestNetmaskForAddress(addressCell.row);
+                        }
+                    }
+                }
+            }
+        }
+
+        ColumnLayout {
+            Layout.alignment: Qt.AlignTop
+            spacing: Kirigami.Units.smallSpacing
+
+            QQC2.Button {
+                Layout.fillWidth: true
+                text: i18n("Add")
+                icon.name: "list-add"
+
+                onClicked: root.setting.addressModel.addRoute()
+            }
+
+            QQC2.Button {
+                Layout.fillWidth: true
+                text: i18n("Remove")
+                icon.name: "list-remove"
+
+                enabled: addressSelection.hasSelection
+
+                onClicked: root.setting.addressModel.removeRoute(addressSelection.currentIndex.row)
+            }
+        }
+    }
+
+    QQC2.CheckBox {
+        Layout.fillWidth: true
+
+        text: i18n("IPv4 is required for this connection")
+
+        checked: root.setting.ipv4Required
+        onToggled: root.setting.ipv4Required = checked
+
+        hoverEnabled: true
+        QQC2.ToolTip.visible: hovered
+        QQC2.ToolTip.delay: Kirigami.Units.humanMoment
+        QQC2.ToolTip.text: i18n("Allows the connection to complete if IPv4 configuration fails but IPv6 configuration succeeds")
+    }
+
+    Item {
+        Layout.fillHeight: true
+    }
+
+    RowLayout {
+        Layout.fillWidth: true
+        spacing: Kirigami.Units.smallSpacing
+
+        Item {
+            Layout.fillWidth: true
+        }
+
+        QQC2.Button {
+            text: i18nc("@action:button", "Advanced…")
+
+            onClicked: advancedDialog.open()
+        }
+
+        QQC2.Button {
+            text: i18nc("@action:button", "Routes…")
+
+            enabled: root.setting.routesEnabled
+
+            onClicked: routesDialog.open()
+        }
+    }
+
+    IPv4Advance {
+        id: advancedDialog
+
+        setting: root.setting
+    }
+
+    IPv4Routes {
+        id: routesDialog
+
+        setting: root.setting
+    }
+
+    DnsList {
+        id: dnsDialog
+
+        onAccepted: root.setting.dns = addresses.join(",")
+    }
+    SearchDomainList {
+        id: searchDomainDialog
+
+        onAccepted: root.setting.dnsSearch = addresses.join(",")
+    }
+}
diff --git a/libs/editorqml/qml/wifisecurity/DnsList.qml b/libs/editorqml/qml/ipv4/SearchDomainList.qml
similarity index 94%
copy from libs/editorqml/qml/wifisecurity/DnsList.qml
copy to libs/editorqml/qml/ipv4/SearchDomainList.qml
index f4927e0c8..516076fa8 100644
--- a/libs/editorqml/qml/wifisecurity/DnsList.qml
+++ b/libs/editorqml/qml/ipv4/SearchDomainList.qml
@@ -1,5 +1,6 @@
 /*
       SPDX-FileCopyrightText: 2026 Tushar Gupta <[email protected]>
+
       SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
 */
 
@@ -12,7 +13,7 @@ import org.kde.plasma.networkmanagement.editorqml
 Kirigami.Dialog {
     id: dialog
 
-    title: i18n("DNS Servers")
+    title: i18n("Search Domains")
     standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
 
     preferredWidth: Kirigami.Units.gridUnit * 28
@@ -37,8 +38,8 @@ Kirigami.Dialog {
                 Layout.fillWidth: true
 
                 validator: RegularExpressionValidator {
-                    // Check for valid ipv4 address
-                    regularExpression: /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
+                    // Check for a valid DNS domain
+                    regularExpression: /^([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?\.)*[A-Za-z]{2,}$/
                 }
 
                 onAccepted: if (addButton.enabled)
diff --git a/libs/editorqml/qml/wificonnectionsettings/WifiConnectionSettings.qml b/libs/editorqml/qml/wificonnectionsettings/WifiConnectionSettings.qml
index c99410abf..ab713e2b4 100644
--- a/libs/editorqml/qml/wificonnectionsettings/WifiConnectionSettings.qml
+++ b/libs/editorqml/qml/wificonnectionsettings/WifiConnectionSettings.qml
@@ -25,7 +25,6 @@ Kirigami.FormLayout {
             anchors.horizontalCenter: parent.horizontalCenter
 
             text: i18n("Connection")
-            font.weight: Font.Bold
             level: 2
         }
     }
@@ -89,7 +88,6 @@ Kirigami.FormLayout {
             anchors.horizontalCenter: parent.horizontalCenter
 
             text: i18n("Advanced")
-            font.weight: Font.Bold
             level: 2
         }
     }
diff --git a/libs/editorqml/qml/wifisecurity/DnsList.qml b/libs/editorqml/qml/wifisecurity/DnsList.qml
index f4927e0c8..35ef1a6f7 100644
--- a/libs/editorqml/qml/wifisecurity/DnsList.qml
+++ b/libs/editorqml/qml/wifisecurity/DnsList.qml
@@ -20,6 +20,9 @@ Kirigami.Dialog {
 
     property alias addresses: listModel.stringList
 
+    // Overridable so the IPv6 page can reuse this dialog with an IPv6 pattern.
+    property var addressPattern: /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
+
     StringListModel {
         id: listModel
     }
@@ -37,8 +40,7 @@ Kirigami.Dialog {
                 Layout.fillWidth: true
 
                 validator: RegularExpressionValidator {
-                    // Check for valid ipv4 address
-                    regularExpression: /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
+                    regularExpression: dialog.addressPattern
                 }
 
                 onAccepted: if (addButton.enabled)
diff --git a/libs/editorqml/settings/generalSettings/generalsettings.h b/libs/editorqml/settings/generalSettings/generalsettings.h
index cbb907d65..6e2eb55b6 100644
--- a/libs/editorqml/settings/generalSettings/generalsettings.h
+++ b/libs/editorqml/settings/generalSettings/generalsettings.h
@@ -93,8 +93,6 @@ private:
     bool m_isVpnConnection = false;
     QVariantList m_vpnConnections;
     QStringList m_firewallZones;
-
-    // WifiSecuritySetting *m_wifisecurity = nullptr;
 };
 
 #endif // PLASMA_NM_GENERAL_SETTING_H
diff --git a/libs/editorqml/settings/ipv4settings/ipv4routetablemodel.cpp b/libs/editorqml/settings/ipv4settings/ipv4routetablemodel.cpp
new file mode 100644
index 000000000..f12961ecc
--- /dev/null
+++ b/libs/editorqml/settings/ipv4settings/ipv4routetablemodel.cpp
@@ -0,0 +1,166 @@
+/*
+    SPDX-FileCopyrightText: 2026 Tushar Gupta <[email protected]>
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "ipv4routetablemodel.h"
+
+#include <KLocalizedString>
+
+RouteTableModel::RouteTableModel(QObject *parent)
+    : QAbstractTableModel(parent)
+{
+}
+
+int RouteTableModel::rowCount(const QModelIndex &parent) const
+{
+    if (parent.isValid())
+        return 0;
+    return static_cast<int>(m_routes.size());
+}
+
+int RouteTableModel::columnCount(const QModelIndex &parent) const
+{
+    if (parent.isValid())
+        return 0;
+    return ColumnCount;
+}
+
+QVariant RouteTableModel::data(const QModelIndex &index, int role) const
+{
+    if (!index.isValid() || index.row() < 0 || index.row() >= m_routes.size())
+        return {};
+
+    if (role != Qt::DisplayRole && role != Qt::EditRole)
+        return {};
+
+    const IpRouteEntry &entry = m_routes.at(index.row());
+
+    switch (index.column()) {
+    case AddressColumn:
+        return entry.address;
+    case NetmaskColumn:
+        return entry.netmask;
+    case NextHopColumn:
+        return entry.nextHop;
+    case MetricColumn:
+        return entry.metric;
+    default:
+        return {};
+    }
+}
+
+bool RouteTableModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+    if (!index.isValid() || index.row() < 0 || index.row() >= m_routes.size())
+        return false;
+
+    if (role != Qt::DisplayRole && role != Qt::EditRole)
+        return false;
+
+    IpRouteEntry &entry = m_routes[index.row()];
+
+    switch (index.column()) {
+    case AddressColumn:
+        if (entry.address == value.toString())
+            return false;
+        entry.address = value.toString();
+        break;
+    case NetmaskColumn:
+        if (entry.netmask == value.toString())
+            return false;
+        entry.netmask = value.toString();
+        break;
+    case NextHopColumn:
+        if (entry.nextHop == value.toString())
+            return false;
+        entry.nextHop = value.toString();
+        break;
+    case MetricColumn: {
+        const uint metric = value.toUInt();
+        if (entry.metric == metric)
+            return false;
+        entry.metric = metric;
+        break;
+    }
+    default:
+        return false;
+    }
+
+    Q_EMIT dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
+    Q_EMIT routesChanged();
+    return true;
+}
+
+QVariant RouteTableModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+    if (role != Qt::DisplayRole)
+        return {};
+
+    if (orientation == Qt::Vertical)
+        return section + 1;
+
+    switch (section) {
+    case AddressColumn:
+        return i18nc("@title:column", "Address");
+    case NetmaskColumn:
+        return i18nc("@title:column", "Netmask");
+    case NextHopColumn:
+        return i18nc("@title:column", "Gateway");
+    case MetricColumn:
+        return i18nc("@title:column", "Metric");
+    default:
+        return {};
+    }
+}
+
+Qt::ItemFlags RouteTableModel::flags(const QModelIndex &index) const
+{
+    if (!index.isValid())
+        return Qt::NoItemFlags;
+    return QAbstractTableModel::flags(index) | Qt::ItemIsEditable;
+}
+
+bool RouteTableModel::setRouteField(int row, int column, const QVariant &value)
+{
+    return setData(index(row, column), value, Qt::EditRole);
+}
+
+QList<IpRouteEntry> RouteTableModel::routes() const
+{
+    return m_routes;
+}
+
+void RouteTableModel::setRoutes(const QList<IpRouteEntry> &routes)
+{
+    beginResetModel();
+    m_routes = routes;
+    endResetModel();
+
+    Q_EMIT routesChanged();
+}
+
+void RouteTableModel::addRoute()
+{
+    const int row = static_cast<int>(m_routes.size());
+
+    beginInsertRows(QModelIndex(), row, row);
+    m_routes.append(IpRouteEntry{});
+    endInsertRows();
+
+    Q_EMIT routesChanged();
+}
+
+void RouteTableModel::removeRoute(int row)
+{
+    if (row < 0 || row >= m_routes.size())
+        return;
+
+    beginRemoveRows(QModelIndex(), row, row);
+    m_routes.removeAt(row);
+    endRemoveRows();
+
+    Q_EMIT routesChanged();
+}
+
+#include "moc_ipv4routetablemodel.cpp"
diff --git a/libs/editorqml/settings/ipv4settings/ipv4routetablemodel.h b/libs/editorqml/settings/ipv4settings/ipv4routetablemodel.h
new file mode 100644
index 000000000..0358168d7
--- /dev/null
+++ b/libs/editorqml/settings/ipv4settings/ipv4routetablemodel.h
@@ -0,0 +1,71 @@
+/*
+    SPDX-FileCopyrightText: 2026 Tushar Gupta <[email protected]>
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef PLASMA_NM_ROUTE_TABLE_MODEL_H
+#define PLASMA_NM_ROUTE_TABLE_MODEL_H
+
+#include "plasmanm_editorqml_export.h"
+
+#include <QAbstractTableModel>
+#include <QList>
+#include <QString>
+#include <QtQmlIntegration/QtQmlIntegration>
+
+struct IpRouteEntry {
+    Q_GADGET
+    QML_VALUE_TYPE(ipRouteEntry)
+
+    Q_PROPERTY(QString address MEMBER address)
+    Q_PROPERTY(QString netmask MEMBER netmask)
+    Q_PROPERTY(QString nextHop MEMBER nextHop)
+    Q_PROPERTY(uint metric MEMBER metric)
+public:
+    QString address;
+    QString netmask;
+    QString nextHop;
+    uint metric = 0;
+};
+
+class PLASMANM_EDITORQML_EXPORT RouteTableModel : public QAbstractTableModel
+{
+    Q_OBJECT
+    QML_ELEMENT
+    QML_UNCREATABLE("")
+
+public:
+    enum Column {
+        AddressColumn = 0,
+        NetmaskColumn,
+        NextHopColumn,
+        MetricColumn,
+        ColumnCount
+    };
+    Q_ENUM(Column)
+
+    explicit RouteTableModel(QObject *parent = nullptr);
+
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+    int columnCount(const QModelIndex &parent = QModelIndex()) const override;
+    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
+    QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+    Qt::ItemFlags flags(const QModelIndex &index) const override;
+
+    Q_INVOKABLE bool setRouteField(int row, int column, const QVariant &value);
+
+    [[nodiscard]] QList<IpRouteEntry> routes() const;
+    void setRoutes(const QList<IpRouteEntry> &routes);
+
+    Q_INVOKABLE void addRoute();
+    Q_INVOKABLE void removeRoute(int row);
+
+Q_SIGNALS:
+    void routesChanged();
+
+private:
+    QList<IpRouteEntry> m_routes;
+};
+
+#endif // PLASMA_NM_ROUTE_TABLE_MODEL_H
diff --git a/libs/editorqml/settings/ipv4settings/ipv4settings.cpp b/libs/editorqml/settings/ipv4settings/ipv4settings.cpp
new file mode 100644
index 000000000..f72aff623
--- /dev/null
+++ b/libs/editorqml/settings/ipv4settings/ipv4settings.cpp
@@ -0,0 +1,478 @@
+/*
+    SPDX-FileCopyrightText: 2026 Tushar Gupta <[email protected]>
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "ipv4settings.h"
+#include "ui_wireguardinterfacewidget.h"
+#include <QHostAddress>
+#include <QHostInfo>
+#include <networkmanagerqt/iproute.h>
+#include <networkmanagerqt/ipv4setting.h>
+#include <qhostaddress.h>
+
+IPv4Settings::IPv4Settings(QObject *parent)
+    : QObject(parent)
+    , m_addressModel(new RouteTableModel(this))
+    , m_routeModel(new RouteTableModel(this))
+{
+    connect(m_routeModel, &RouteTableModel::routesChanged, this, [this] {
+        Q_EMIT routesChanged();
+        Q_EMIT validChanged();
+    });
+    connect(m_addressModel, &RouteTableModel::routesChanged, this, [this] {
+        Q_EMIT addressesChanged();
+        Q_EMIT validChanged();
+    });
+}
+
+quint32 IPv4Settings::suggestNetmask(quint32 ip)
+{
+    /*
+        A   0       0.0.0.0 <-->127.255.255.255  255.0.0.0 <--->/8
+        B   10      128.0.0.0 <>191.255.255.255  255.255.0.0 <->/16
+        C   110     192.0.0.0 <>223.255.255.255  255.255.255.0 >/24
+        D   1110    224.0.0.0 <>239.255.255.255  not defined <->not defined
+        E   1111    240.0.0.0 <>255.255.255.254  not defined <->not defined
+    */
+    quint32 netmask = 0;
+
+    if (!(ip & 0x80000000)) {
+        // test 0 leading bit
+        netmask = 0xFF000000;
+    } else if (!(ip & 0x40000000)) {
+        // test 10 leading bits
+        netmask = 0xFFFF0000;
+    } else if (!(ip & 0x20000000)) {
+        // test 110 leading bits
+        netmask = 0xFFFFFF00;
+    }
+
+    return netmask;
+}
+
+void IPv4Settings::loadConfig(const NetworkManager::Ipv4Setting::Ptr &setting)
+{
+    if (!setting)
+        return;
+
+    QList<IpRouteEntry> routeEntries;
+    for (const NetworkManager::IpRoute &r : setting->routes()) {
+        IpRouteEntry entry;
+        entry.address = r.ip().toString();
+        entry.netmask = r.netmask().toString();
+        entry.nextHop = r.nextHop().toString();
+        entry.metric = r.metric();
+        routeEntries.append(entry);
+    }
+    m_routeModel->setRoutes(routeEntries);
+    m_neverDefault = setting->neverDefault();
+    m_ignoreAutoRoutes = setting->ignoreAutoRoutes();
+    m_dhcpHostname = setting->dhcpHostname();
+    m_dhcpSendHostname = setting->dhcpSendHostname();
+    m_dadTimeout = setting->dadTimeout();
+
+    switch (setting->method()) {
+    case NetworkManager::Ipv4Setting::Automatic:
+        m_method = setting->ignoreAutoDns() ? AutomaticOnlyIP : Automatic;
+        break;
+    case NetworkManager::Ipv4Setting::Manual:
+        m_method = Manual;
+        break;
+    case NetworkManager::Ipv4Setting::LinkLocal:
+        m_method = LinkLocal;
+        break;
+    case NetworkManager::Ipv4Setting::Shared:
+        m_method = Shared;
+        break;
+    case NetworkManager::Ipv4Setting::Disabled:
+        m_method = Disabled;
+        break;
+    }
+
+    QStringList tmp;
+    for (const QHostAddress &addr : setting->dns()) {
+        tmp.append(addr.toString());
+    }
+
+    // dns
+    m_dns = tmp.join(QStringLiteral(","));
+    m_dnsSearch = setting->dnsSearch().join(QLatin1Char(','));
+    m_dhcpClientId = setting->dhcpClientId();
+
+    // metric
+    m_routeMetric = static_cast<double>(setting->routeMetric());
+
+    // addresses
+    QList<IpRouteEntry> addressEntries;
+    for (const NetworkManager::IpAddress &addr : setting->addresses()) {
+        IpRouteEntry entry;
+        entry.address = addr.ip().toString();
+        entry.netmask = addr.netmask().toString();
+        entry.nextHop = addr.gateway().toString();
+        addressEntries.append(entry);
+    }
+    m_addressModel->setRoutes(addressEntries);
+
+    m_ipv4Required = !setting->mayFail();
+
+    Q_EMIT methodChanged();
+    Q_EMIT dnsChanged();
+    Q_EMIT dnsSearchChanged();
+    Q_EMIT dhcpClientIdChanged();
+    Q_EMIT routeMetricChanged();
+    Q_EMIT addressesChanged();
+    Q_EMIT ipv4RequiredChanged();
+    Q_EMIT routesChanged();
+    Q_EMIT validChanged();
+}
+
+QVariantMap IPv4Settings::setting() const
+{
+    NetworkManager::Ipv4Setting ipv4;
+    QList<NetworkManager::IpRoute> nmRoutes;
+
+    const QList<IpRouteEntry> routeEntries = m_routeModel->routes();
+    for (const IpRouteEntry &r : routeEntries) {
+        const QHostAddress ip(r.address);
+        const QHostAddress netmask(r.netmask);
+        if (ip.isNull() || netmask.isNull())
+            continue;
+
+        NetworkManager::IpRoute route;
+        route.setIp(ip);
+        route.setNetmask(netmask);
+        route.setNextHop(QHostAddress(r.nextHop));
+        route.setMetric(r.metric);
+        nmRoutes.append(route);
+    }
+
+    ipv4.setRoutes(nmRoutes);
+    ipv4.setNeverDefault(m_neverDefault);
+    ipv4.setIgnoreAutoRoutes(m_ignoreAutoRoutes);
+    ipv4.setDhcpHostname(m_dhcpHostname);
+    ipv4.setDhcpSendHostname(m_dhcpSendHostname);
+    ipv4.setDadTimeout(m_dadTimeout);
+
+    // method
+    switch (m_method) {
+    case Automatic:
+        ipv4.setMethod(NetworkManager::Ipv4Setting::Automatic);
+        break;
+    case AutomaticOnlyIP:
+        ipv4.setMethod(NetworkManager::Ipv4Setting::Automatic);
+        ipv4.setIgnoreAutoDns(true);
+        break;
+    case Manual:
+        ipv4.setMethod(NetworkManager::Ipv4Setting::Manual);
+        break;
+    case LinkLocal:
+        ipv4.setMethod(NetworkManager::Ipv4Setting::LinkLocal);
+        break;
+    case Shared:
+        ipv4.setMethod(NetworkManager::Ipv4Setting::Shared);
+        break;
+    case Disabled:
+        ipv4.setMethod(NetworkManager::Ipv4Setting::Disabled);
+        break;
+    }
+
+    // dns
+    if (dnsEnabled() && !m_dns.isEmpty()) {
+        QList<QHostAddress> addrs;
+        for (const QString &s : m_dns.split(QLatin1Char(','))) {
+            QHostAddress a(s.trimmed());
+            if (!a.isNull())
+                addrs.append(a);
+        }
+        ipv4.setDns(addrs);
+    }
+    if (dnsEnabled() && !m_dnsSearch.isEmpty()) {
+        ipv4.setDnsSearch(m_dnsSearch.split(QLatin1Char(',')));
+    }
+
+    // dhcp id
+    if (dhcpClientIdEnabled() && !m_dhcpClientId.isEmpty()) {
+        ipv4.setDhcpClientId(m_dhcpClientId);
+    }
+
+    // metric
+    ipv4.setRouteMetric(static_cast<int>(m_routeMetric));
+
+    // addresses
+    if (addressTableEnabled()) {
+        QList<NetworkManager::IpAddress> list;
+        const QList<IpRouteEntry> addressEntries = m_addressModel->routes();
+        for (const IpRouteEntry &e : addressEntries) {
+            const QHostAddress ip(e.address);
+            const QHostAddress netmask(e.netmask);
+            if (ip.isNull() || netmask.isNull())
+                continue;
+
+            NetworkManager::IpAddress addr;
+            addr.setIp(ip);
+            addr.setNetmask(netmask);
+            if (!e.nextHop.isEmpty())
+                addr.setGateway(QHostAddress(e.nextHop));
+            list.append(addr);
+        }
+
+        if (!list.isEmpty()) {
+            ipv4.setAddresses(list);
+        }
+    }
+
+    ipv4.setMayFail(!m_ipv4Required);
+
+    return ipv4.toMap();
+}
+
+QString IPv4Settings::dnsLabel() const
+{
+    return m_method == Automatic ? i18n("Other DNS Servers:") : i18n("DNS Servers:");
+}
+
+bool IPv4Settings::dnsEnabled() const
+{
+    if (m_method == LinkLocal || m_method == Shared || m_method == Disabled)
+        return false;
+    return true;
+}
+
+bool IPv4Settings::dhcpClientIdEnabled() const
+{
+    return m_method == Automatic || m_method == AutomaticOnlyIP;
+}
+
+bool IPv4Settings::addressTableEnabled() const
+{
+    return m_method == Manual;
+}
+
+bool IPv4Settings::routesEnabled() const
+{
+    return m_method != LinkLocal && m_method != Shared && m_method != Disabled;
+}
+
+bool IPv4Settings::isValid() const
+{
+    if (m_method == Manual) {
+        const QList<IpRouteEntry> addressEntries = m_addressModel->routes();
+        if (addressEntries.isEmpty())
+            return false;
+        for (const IpRouteEntry &e : addressEntries) {
+            QHostAddress ip(e.address);
+            QHostAddress netmask(e.netmask);
+            QHostAddress gw(e.nextHop);
+            if (ip.isNull() || netmask.isNull() || (gw.isNull() && !e.nextHop.isEmpty())) {
+                return false;
+            }
+        }
+    }
+
+    if (!m_dns.isEmpty() && (m_method == Automatic || m_method == Manual || m_method == AutomaticOnlyIP)) {
+        for (const QString &s : m_dns.split(QLatin1Char(','))) {
+            if (QHostAddress(s.trimmed()).isNull()) {
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
+void IPv4Settings::suggestNetmaskForAddress(int index)
+{
+    const QList<IpRouteEntry> addressEntries = m_addressModel->routes();
+    if (index < 0 || index >= addressEntries.size())
+        return;
+
+    const IpRouteEntry &entry = addressEntries.at(index);
+    if (!entry.netmask.isEmpty())
+        return; // don't overwrite existing
+
+    QHostAddress addr(entry.address);
+    const quint32 netmask = suggestNetmask(addr.toIPv4Address());
+    if (netmask) {
+        m_addressModel->setRouteField(index, RouteTableModel::NetmaskColumn, QHostAddress(netmask).toString());
+    }
+}
+
+void IPv4Settings::setMethod(MethodIndex m)
+{
+    if (m_method == m)
+        return;
+    m_method = m;
+    Q_EMIT methodChanged();
+    Q_EMIT validChanged();
+}
+
+IPv4Settings::MethodIndex IPv4Settings::method() const
+{
+    return m_method;
+}
+
+QString IPv4Settings::dns() const
+{
+    return m_dns;
+}
+void IPv4Settings::setDns(const QString &v)
+{
+    if (m_dns == v)
+        return;
+    m_dns = v;
+    Q_EMIT dnsChanged();
+    Q_EMIT validChanged();
+}
+
+QString IPv4Settings::dnsSearch() const
+{
+    return m_dnsSearch;
+}
+void IPv4Settings::setDnsSearch(const QString &v)
+{
+    if (m_dnsSearch == v)
+        return;
+    m_dnsSearch = v;
+    Q_EMIT dnsSearchChanged();
+    Q_EMIT validChanged();
+}
+
+QString IPv4Settings::dhcpClientId() const
+{
+    return m_dhcpClientId;
+}
+void IPv4Settings::setDhcpClientId(const QString &v)
+{
+    if (m_dhcpClientId == v)
+        return;
+    m_dhcpClientId = v;
+    Q_EMIT dhcpClientIdChanged();
+    Q_EMIT validChanged();
+}
+
+double IPv4Settings::routeMetric() const
+{
+    return m_routeMetric;
+}
+void IPv4Settings::setRouteMetric(double v)
+{
+    if (m_routeMetric == v)
+        return;
+    m_routeMetric = v;
+    Q_EMIT routeMetricChanged();
+    Q_EMIT validChanged();
+}
+
+QList<IpRouteEntry> IPv4Settings::addresses() const
+{
+    return m_addressModel->routes();
+}
+void IPv4Settings::setAddresses(const QList<IpRouteEntry> &v)
+{
+    m_addressModel->setRoutes(v);
+}
+
+RouteTableModel *IPv4Settings::addressModel() const
+{
+    return m_addressModel;
+}
+
+bool IPv4Settings::ipv4Required() const
+{
+    return m_ipv4Required;
+}
+void IPv4Settings::setIpv4Required(bool v)
+{
+    if (m_ipv4Required == v)
+        return;
+    m_ipv4Required = v;
+    Q_EMIT ipv4RequiredChanged();
+    Q_EMIT validChanged();
+}
+
+QList<IpRouteEntry> IPv4Settings::routes() const
+{
+    return m_routeModel->routes();
+}
+void IPv4Settings::setRoutes(const QList<IpRouteEntry> &v)
+{
+    // The model emits routesChanged() for us.
+    m_routeModel->setRoutes(v);
+}
+
+RouteTableModel *IPv4Settings::routeModel() const
+{
+    return m_routeModel;
+}
+
+bool IPv4Settings::dhcpSendHostname() const
+{
+    return m_dhcpSendHostname;
+}
+void IPv4Settings::setDhcpSendHostname(bool v)
+{
+    if (m_dhcpSendHostname == v)
+        return;
+    m_dhcpSendHostname = v;
+    Q_EMIT dhcpSendHostnameChanged();
+    Q_EMIT validChanged();
+}
+
+QString IPv4Settings::dhcpHostname() const
+{
+    return m_dhcpHostname;
+}
+void IPv4Settings::setDhcpHostname(const QString &v)
+{
+    if (m_dhcpHostname == v)
+        return;
+    m_dhcpHostname = v;
+    Q_EMIT dhcpHostnameChanged();
+    Q_EMIT validChanged();
+}
+
+QString IPv4Settings::systemHostname() const
+{
+    return QHostInfo::localHostName();
+}
+
+int IPv4Settings::dadTimeout() const
+{
+    return m_dadTimeout;
+}
+void IPv4Settings::setDadTimeout(int v)
+{
+    if (m_dadTimeout == v)
+        return;
+    m_dadTimeout = v;
+    Q_EMIT dadTimeoutChanged();
+    Q_EMIT validChanged();
+}
+
+bool IPv4Settings::neverDefault() const
+{
+    return m_neverDefault;
+}
+void IPv4Settings::setNeverDefault(bool v)
+{
+    if (m_neverDefault == v)
+        return;
+    m_neverDefault = v;
+    Q_EMIT neverDefaultChanged();
+    Q_EMIT validChanged();
+}
+
+bool IPv4Settings::ignoreAutoRoutes() const
+{
+    return m_ignoreAutoRoutes;
+}
+void IPv4Settings::setIgnoreAutoRoutes(bool v)
+{
+    if (m_ignoreAutoRoutes == v)
+        return;
+    m_ignoreAutoRoutes = v;
+    Q_EMIT ignoreAutoRoutesChanged();
+    Q_EMIT validChanged();
+}
+
+#include "moc_ipv4settings.cpp"
diff --git a/libs/editorqml/settings/ipv4settings/ipv4settings.h b/libs/editorqml/settings/ipv4settings/ipv4settings.h
new file mode 100644
index 000000000..16f840276
--- /dev/null
+++ b/libs/editorqml/settings/ipv4settings/ipv4settings.h
@@ -0,0 +1,156 @@
+/*
+    SPDX-FileCopyrightText: 2026 Tushar Gupta <[email protected]>
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef PLASMA_NM_IPV4_WIDGET_NH
+#define PLASMA_NM_IPV4_WIDGET_NH
+
+#include "ipv4routetablemodel.h"
+#include "plasmanm_editorqml_export.h"
+
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QtQmlIntegration/QtQmlIntegration>
+
+#include <NetworkManagerQt/IpAddress>
+#include <NetworkManagerQt/IpRoute>
+#include <NetworkManagerQt/Ipv4Setting>
+
+class PLASMANM_EDITORQML_EXPORT IPv4Settings : public QObject
+{
+    Q_OBJECT
+    QML_NAMED_ELEMENT(IPv4Setting)
+    QML_UNCREATABLE("")
+
+    Q_PROPERTY(MethodIndex method READ method WRITE setMethod NOTIFY methodChanged)
+
+    Q_PROPERTY(QString dns READ dns WRITE setDns NOTIFY dnsChanged)
+    Q_PROPERTY(QString dnsSearch READ dnsSearch WRITE setDnsSearch NOTIFY dnsSearchChanged)
+    Q_PROPERTY(QString dhcpClientId READ dhcpClientId WRITE setDhcpClientId NOTIFY dhcpClientIdChanged)
+    Q_PROPERTY(QString dnsLabel READ dnsLabel NOTIFY methodChanged)
+
+    Q_PROPERTY(double routeMetric READ routeMetric WRITE setRouteMetric NOTIFY routeMetricChanged)
+
+    Q_PROPERTY(QList<IpRouteEntry> addresses READ addresses WRITE setAddresses NOTIFY addressesChanged)
+    Q_PROPERTY(RouteTableModel *addressModel READ addressModel CONSTANT)
+
+    Q_PROPERTY(bool ipv4Required READ ipv4Required WRITE setIpv4Required NOTIFY ipv4RequiredChanged)
+
+    Q_PROPERTY(QList<IpRouteEntry> routes READ routes WRITE setRoutes NOTIFY routesChanged)
+
+    Q_PROPERTY(RouteTableModel *routeModel READ routeModel CONSTANT)
+
+    // advanced dialog fields
+    Q_PROPERTY(bool dhcpSendHostname READ dhcpSendHostname WRITE setDhcpSendHostname NOTIFY dhcpSendHostnameChanged)
+    Q_PROPERTY(QString dhcpHostname READ dhcpHostname WRITE setDhcpHostname NOTIFY dhcpHostnameChanged)
+
+    Q_PROPERTY(QString systemHostname READ systemHostname CONSTANT)
+    Q_PROPERTY(int dadTimeout READ dadTimeout WRITE setDadTimeout NOTIFY dadTimeoutChanged)
+    Q_PROPERTY(bool neverDefault READ neverDefault WRITE setNeverDefault NOTIFY neverDefaultChanged)
+    Q_PROPERTY(bool ignoreAutoRoutes READ ignoreAutoRoutes WRITE setIgnoreAutoRoutes NOTIFY ignoreAutoRoutesChanged)
+
+    Q_PROPERTY(bool dnsEnabled READ dnsEnabled NOTIFY methodChanged)
+    Q_PROPERTY(bool dhcpClientIdEnabled READ dhcpClientIdEnabled NOTIFY methodChanged)
+    Q_PROPERTY(bool addressTableEnabled READ addressTableEnabled NOTIFY methodChanged)
+    Q_PROPERTY(bool routesEnabled READ routesEnabled NOTIFY methodChanged)
+
+    Q_PROPERTY(bool valid READ isValid NOTIFY validChanged)
+
+public:
+    enum MethodIndex {
+        Automatic = 0,
+        AutomaticOnlyIP,
+        LinkLocal,
+        Manual,
+        Shared,
+        Disabled
+    };
+    Q_ENUM(MethodIndex)
+
+    explicit IPv4Settings(QObject *parent = nullptr);
+
+    Q_INVOKABLE void loadConfig(const NetworkManager::Ipv4Setting::Ptr &setting);
+
+    Q_INVOKABLE QVariantMap setting() const;
+
+    // property getters
+    MethodIndex method() const;
+    QString dns() const;
+    QString dnsSearch() const;
+    QString dhcpClientId() const;
+    double routeMetric() const;
+    QList<IpRouteEntry> addresses() const;
+    RouteTableModel *addressModel() const;
+    bool ipv4Required() const;
+    QList<IpRouteEntry> routes() const;
+    RouteTableModel *routeModel() const;
+    bool dhcpSendHostname() const;
+    QString dhcpHostname() const;
+    QString systemHostname() const;
+    int dadTimeout() const;
+    bool neverDefault() const;
+    bool ignoreAutoRoutes() const;
+
+    bool dnsEnabled() const;
+    bool dhcpClientIdEnabled() const;
+    bool addressTableEnabled() const;
+    bool routesEnabled() const;
+    QString dnsLabel() const;
+
+    bool isValid() const;
+
+    // property setters
+    void setMethod(MethodIndex method);
+    void setDns(const QString &dns);
+    void setDnsSearch(const QString &dnsSearch);
+    void setDhcpClientId(const QString &id);
+    void setRouteMetric(double metric);
+    void setAddresses(const QList<IpRouteEntry> &addresses);
+    void setIpv4Required(bool required);
+    void setRoutes(const QList<IpRouteEntry> &routes);
+    void setDhcpSendHostname(bool send);
+    void setDhcpHostname(const QString &hostname);
+    void setDadTimeout(int timeout);
+    void setNeverDefault(bool neverDefault);
+    void setIgnoreAutoRoutes(bool ignore);
+
+    Q_INVOKABLE void suggestNetmaskForAddress(int index);
+
+Q_SIGNALS:
+    void methodChanged();
+    void dnsChanged();
+    void dnsSearchChanged();
+    void dhcpClientIdChanged();
+    void routeMetricChanged();
+    void addressesChanged();
+    void ipv4RequiredChanged();
+    void routesChanged();
+    void dhcpSendHostnameChanged();
+    void dhcpHostnameChanged();
+    void dadTimeoutChanged();
+    void neverDefaultChanged();
+    void ignoreAutoRoutesChanged();
+    void validChanged();
+
+private:
+    quint32 suggestNetmask(quint32 ip);
+
+    MethodIndex m_method = Automatic;
+    QString m_dns;
+    QString m_dnsSearch;
+    QString m_dhcpClientId;
+    double m_routeMetric = -1;
+    RouteTableModel *m_addressModel = nullptr;
+    bool m_ipv4Required = true;
+
+    RouteTableModel *m_routeModel = nullptr;
+    bool m_dhcpSendHostname = true;
+    QString m_dhcpHostname;
+    int m_dadTimeout = -1;
+    bool m_neverDefault = false;
+    bool m_ignoreAutoRoutes = false;
+};
+
+#endif
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.