[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