diff --git a/pcsx2-qt/CMakeLists.txt b/pcsx2-qt/CMakeLists.txt
index 6e99bee910..f1702e01c3 100644
--- a/pcsx2-qt/CMakeLists.txt
+++ b/pcsx2-qt/CMakeLists.txt
@@ -86,6 +86,11 @@ target_sources(pcsx2-qt PRIVATE
Settings/MemoryCardSettingsWidget.cpp
Settings/MemoryCardSettingsWidget.h
Settings/MemoryCardSettingsWidget.ui
+ Settings/DEV9SettingsWidget.cpp
+ Settings/DEV9SettingsWidget.h
+ Settings/DEV9SettingsWidget.ui
+ Settings/HddCreateQt.cpp
+ Settings/HddCreateQt.h
Settings/SettingsDialog.cpp
Settings/SettingsDialog.h
Settings/SettingsDialog.ui
diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp
index 85fc142fa4..b50aa9faeb 100644
--- a/pcsx2-qt/MainWindow.cpp
+++ b/pcsx2-qt/MainWindow.cpp
@@ -147,6 +147,7 @@ void MainWindow::connectSignals()
connect(m_ui.actionGraphicsSettings, &QAction::triggered, [this]() { doSettings("Graphics"); });
connect(m_ui.actionAudioSettings, &QAction::triggered, [this]() { doSettings("Audio"); });
connect(m_ui.actionMemoryCardSettings, &QAction::triggered, [this]() { doSettings("Memory Cards"); });
+ connect(m_ui.actionDEV9Settings, &QAction::triggered, [this]() { doSettings("Network & HDD"); });
connect(
m_ui.actionControllerSettings, &QAction::triggered, [this]() { doControllerSettings(ControllerSettingsDialog::Category::GlobalSettings); });
connect(m_ui.actionHotkeySettings, &QAction::triggered, [this]() { doControllerSettings(ControllerSettingsDialog::Category::HotkeySettings); });
diff --git a/pcsx2-qt/MainWindow.ui b/pcsx2-qt/MainWindow.ui
index c8bee08e66..7de092f1ac 100644
--- a/pcsx2-qt/MainWindow.ui
+++ b/pcsx2-qt/MainWindow.ui
@@ -111,6 +111,7 @@
+
@@ -555,6 +556,14 @@
&Memory Cards
+
+
+ &Network && HDD
+
+
+
+
+
true
diff --git a/pcsx2-qt/Settings/DEV9SettingsWidget.cpp b/pcsx2-qt/Settings/DEV9SettingsWidget.cpp
new file mode 100644
index 0000000000..06034cb466
--- /dev/null
+++ b/pcsx2-qt/Settings/DEV9SettingsWidget.cpp
@@ -0,0 +1,830 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#include "PrecompiledHeader.h"
+
+#include
+#include
+#include
+
+#include "common/StringUtil.h"
+#include "DEV9SettingsWidget.h"
+#include "EmuThread.h"
+#include "QtUtils.h"
+#include "SettingWidgetBinder.h"
+#include "SettingsDialog.h"
+
+#include "HddCreateQt.h"
+
+#include "DEV9/pcap_io.h"
+#ifdef _WIN32
+#include "DEV9/Win32/tap.h"
+#endif
+
+static const char* s_api_name[] = {
+ QT_TRANSLATE_NOOP("DEV9SettingsWidget", " "),
+ QT_TRANSLATE_NOOP("DEV9SettingsWidget", "PCAP Bridged"),
+ QT_TRANSLATE_NOOP("DEV9SettingsWidget", "PCAP Switched"),
+ QT_TRANSLATE_NOOP("DEV9SettingsWidget", "TAP"),
+ nullptr,
+};
+
+static const char* s_dns_name[] = {
+ QT_TRANSLATE_NOOP("DEV9SettingsWidget", "Manual"),
+ QT_TRANSLATE_NOOP("DEV9SettingsWidget", "Auto"),
+ QT_TRANSLATE_NOOP("DEV9SettingsWidget", "Internal"),
+ nullptr,
+};
+
+using PacketReader::IP::IP_Address;
+
+#define IP_RANGE_INTER "([0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5]|)"
+#define IP_RANGE_FINAL "([0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])"
+
+// clang-format off
+const QRegularExpression IPValidator::intermediateRegex{QStringLiteral("^" IP_RANGE_INTER "\\." IP_RANGE_INTER "\\." IP_RANGE_INTER "\\." IP_RANGE_INTER "$")};
+const QRegularExpression IPValidator::finalRegex {QStringLiteral("^" IP_RANGE_FINAL "\\." IP_RANGE_FINAL "\\." IP_RANGE_FINAL "\\." IP_RANGE_FINAL "$")};
+// clang-format on
+
+IPValidator::IPValidator(QObject* parent, bool allowEmpty)
+ : QValidator(parent)
+ , m_allowEmpty{allowEmpty}
+{
+}
+
+QValidator::State IPValidator::validate(QString& input, int& pos) const
+{
+ if (input.isEmpty())
+ return m_allowEmpty ? Acceptable : Intermediate;
+
+ QRegularExpressionMatch m = finalRegex.match(input, 0, QRegularExpression::NormalMatch);
+ if (m.hasMatch())
+ return Acceptable;
+
+ m = intermediateRegex.match(input, 0, QRegularExpression::PartialPreferCompleteMatch);
+ if (m.hasMatch() || m.hasPartialMatch())
+ return Intermediate;
+ else
+ {
+ pos = input.size();
+ return Invalid;
+ }
+}
+
+IPItemDelegate::IPItemDelegate(QObject* parent)
+ : QItemDelegate(parent)
+{
+}
+
+QWidget* IPItemDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
+{
+ QLineEdit* editor = new QLineEdit(parent);
+ editor->setValidator(new IPValidator());
+ return editor;
+}
+
+void IPItemDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
+{
+ QString value = index.model()->data(index, Qt::EditRole).toString();
+ QLineEdit* line = static_cast(editor);
+ line->setText(value);
+}
+
+void IPItemDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
+{
+ QLineEdit* line = static_cast(editor);
+ QString value = line->text();
+ model->setData(index, value);
+}
+
+void IPItemDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const
+{
+ editor->setGeometry(option.rect);
+}
+
+
+DEV9SettingsWidget::DEV9SettingsWidget(SettingsDialog* dialog, QWidget* parent)
+ : QWidget(parent)
+ , m_dialog{dialog}
+{
+ SettingsInterface* sif = dialog->getSettingsInterface();
+
+ m_ui.setupUi(this);
+
+ //////////////////////////////////////////////////////////////////////////
+ // Eth Enabled
+ //////////////////////////////////////////////////////////////////////////
+ //Connect needs to be after BindWidgetToBoolSetting to ensure correct order of execution for disabling a per game setting
+ //but we then need to manually call onEthAutoChanged to update the UI on fist load
+ SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.ethEnabled, "DEV9/Eth", "EthEnable", false);
+ onEthEnabledChanged(m_ui.ethEnabled->checkState());
+ connect(m_ui.ethEnabled, QOverload::of(&QCheckBox::stateChanged), this, &DEV9SettingsWidget::onEthEnabledChanged);
+
+ //////////////////////////////////////////////////////////////////////////
+ // Eth Device Settings
+ //////////////////////////////////////////////////////////////////////////
+ connect(m_ui.ethDevType, QOverload::of(&QComboBox::currentIndexChanged), this, &DEV9SettingsWidget::onEthDeviceTypeChanged);
+
+ m_api_list.push_back(Pcsx2Config::DEV9Options::NetApi::Unset);
+
+ for (const AdapterEntry& adapter : PCAPAdapter::GetAdapters())
+ AddAdapter(adapter);
+#ifdef _WIN32
+ for (const AdapterEntry& adapter : TAPAdapter::GetAdapters())
+ AddAdapter(adapter);
+#endif
+
+ std::sort(m_api_list.begin(), m_api_list.end());
+ for (auto& list : m_adapter_list)
+ std::sort(list.begin(), list.end(), [](const AdapterEntry& a, AdapterEntry& b) { return a.name < b.name; });
+
+ for (const Pcsx2Config::DEV9Options::NetApi& na : m_api_list)
+ {
+ m_api_namelist.push_back(s_api_name[static_cast(na)]);
+ m_api_valuelist.push_back(Pcsx2Config::DEV9Options::NetApiNames[static_cast(na)]);
+ }
+
+ m_api_namelist.push_back(nullptr);
+ m_api_valuelist.push_back(nullptr);
+
+ //We replace the blank entry with one for global settings
+ Pcsx2Config::DEV9Options::NetApi baseAPI = Pcsx2Config::DEV9Options::NetApi::Unset;
+ if (m_dialog->isPerGameSettings())
+ {
+ const std::string valueAPI = QtHost::GetBaseStringSettingValue("DEV9/Eth", "EthApi", Pcsx2Config::DEV9Options::NetApiNames[static_cast(Pcsx2Config::DEV9Options::NetApi::Unset)]);
+ for (int i = 0; Pcsx2Config::DEV9Options::NetApiNames[i] != nullptr; i++)
+ {
+ if (valueAPI == Pcsx2Config::DEV9Options::NetApiNames[i])
+ {
+ baseAPI = static_cast(i);
+ break;
+ }
+ }
+
+ std::vector baseList = m_adapter_list[static_cast(baseAPI)];
+
+ std::string baseAdapter = " ";
+ const std::string valueGUID = QtHost::GetBaseStringSettingValue("DEV9/Eth", "EthDevice", "");
+ for (size_t i = 0; i < baseList.size(); i++)
+ {
+ if (baseList[i].guid == valueGUID)
+ {
+ baseAdapter = baseList[i].name;
+ break;
+ }
+ }
+
+ m_adapter_list[static_cast(Pcsx2Config::DEV9Options::NetApi::Unset)][0].name = baseAdapter;
+ }
+
+ if (m_dialog->isPerGameSettings())
+ m_ui.ethDevType->addItem(tr("Use Global Setting [%1]").arg(QString::fromUtf8(Pcsx2Config::DEV9Options::NetApiNames[static_cast(baseAPI)])));
+ else
+ m_ui.ethDevType->addItem(QString::fromUtf8(m_api_namelist[0]));
+
+ for (int i = 1; m_api_namelist[i] != nullptr; i++)
+ m_ui.ethDevType->addItem(QString::fromUtf8(m_api_namelist[i]));
+
+ const std::string value = m_dialog->getStringValue("DEV9/Eth", "EthApi", Pcsx2Config::DEV9Options::NetApiNames[static_cast(Pcsx2Config::DEV9Options::NetApi::Unset)]).value();
+
+ for (int i = 0; m_api_namelist[i] != nullptr; i++)
+ {
+ if (value == m_api_valuelist[i])
+ {
+ m_ui.ethDevType->setCurrentIndex(i);
+ break;
+ }
+ }
+ //onEthDeviceTypeChanged gets called automatically
+
+ connect(m_ui.ethDev, QOverload::of(&QComboBox::currentIndexChanged), this, &DEV9SettingsWidget::onEthDeviceChanged);
+
+ //////////////////////////////////////////////////////////////////////////
+ // DHCP Settings
+ //////////////////////////////////////////////////////////////////////////
+ SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.ethInterceptDHCP, "DEV9/Eth", "InterceptDHCP", false);
+ onEthDHCPInterceptChanged(m_ui.ethInterceptDHCP->checkState());
+ connect(m_ui.ethInterceptDHCP, QOverload::of(&QCheckBox::stateChanged), this, &DEV9SettingsWidget::onEthDHCPInterceptChanged);
+
+ //IP settings
+ const IPValidator* ipValidator = new IPValidator(this, m_dialog->isPerGameSettings());
+
+ // clang-format off
+ m_ui.ethPS2Addr ->setValidator(ipValidator);
+ m_ui.ethNetMask ->setValidator(ipValidator);
+ m_ui.ethGatewayAddr->setValidator(ipValidator);
+ m_ui.ethDNS1Addr ->setValidator(ipValidator);
+ m_ui.ethDNS2Addr ->setValidator(ipValidator);
+
+ if (m_dialog->isPerGameSettings())
+ {
+ m_ui.ethPS2Addr ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "PS2IP", "").value().c_str()));
+ m_ui.ethNetMask ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "Mask", "").value().c_str()));
+ m_ui.ethGatewayAddr->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "Gateway", "").value().c_str()));
+ m_ui.ethDNS1Addr ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "DNS1", "").value().c_str()));
+ m_ui.ethDNS2Addr ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "DNS2", "").value().c_str()));
+
+ m_ui.ethPS2Addr ->setPlaceholderText(QString::fromUtf8(QtHost::GetBaseStringSettingValue("DEV9/Eth", "PS2IP", "0.0.0.0").c_str()));
+ m_ui.ethNetMask ->setPlaceholderText(QString::fromUtf8(QtHost::GetBaseStringSettingValue("DEV9/Eth", "Mask", "0.0.0.0").c_str()));
+ m_ui.ethGatewayAddr->setPlaceholderText(QString::fromUtf8(QtHost::GetBaseStringSettingValue("DEV9/Eth", "Gateway", "0.0.0.0").c_str()));
+ m_ui.ethDNS1Addr ->setPlaceholderText(QString::fromUtf8(QtHost::GetBaseStringSettingValue("DEV9/Eth", "DNS1", "0.0.0.0").c_str()));
+ m_ui.ethDNS2Addr ->setPlaceholderText(QString::fromUtf8(QtHost::GetBaseStringSettingValue("DEV9/Eth", "DNS2", "0.0.0.0").c_str()));
+ }
+ else
+ {
+ m_ui.ethPS2Addr ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "PS2IP", "0.0.0.0").value().c_str()));
+ m_ui.ethNetMask ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "Mask", "0.0.0.0").value().c_str()));
+ m_ui.ethGatewayAddr->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "Gateway", "0.0.0.0").value().c_str()));
+ m_ui.ethDNS1Addr ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "DNS1", "0.0.0.0").value().c_str()));
+ m_ui.ethDNS2Addr ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "DNS2", "0.0.0.0").value().c_str()));
+ }
+
+ connect(m_ui.ethPS2Addr, &QLineEdit::editingFinished, this, [&]() { onEthIPChanged(m_ui.ethPS2Addr, "DEV9/Eth", "PS2IP" ); });
+ connect(m_ui.ethNetMask, &QLineEdit::editingFinished, this, [&]() { onEthIPChanged(m_ui.ethNetMask, "DEV9/Eth", "Mask" ); });
+ connect(m_ui.ethGatewayAddr, &QLineEdit::editingFinished, this, [&]() { onEthIPChanged(m_ui.ethGatewayAddr, "DEV9/Eth", "Gateway"); });
+ connect(m_ui.ethDNS1Addr, &QLineEdit::editingFinished, this, [&]() { onEthIPChanged(m_ui.ethDNS1Addr, "DEV9/Eth", "DNS1" ); });
+ connect(m_ui.ethDNS2Addr, &QLineEdit::editingFinished, this, [&]() { onEthIPChanged(m_ui.ethDNS2Addr, "DEV9/Eth", "DNS2" ); });
+ // clang-format on
+
+ //Auto
+ SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.ethNetMaskAuto, "DEV9/Eth", "AutoMask", true);
+ onEthAutoChanged(m_ui.ethNetMaskAuto, m_ui.ethNetMaskAuto->checkState(), m_ui.ethNetMask, "DEV9/Eth", "AutoMask");
+ connect(m_ui.ethNetMaskAuto, QOverload::of(&QCheckBox::stateChanged), this, [&](int state) { onEthAutoChanged(m_ui.ethNetMaskAuto, state, m_ui.ethNetMask, "DEV9/Eth", "AutoMask"); });
+
+ SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.ethGatewayAuto, "DEV9/Eth", "AutoGateway", true);
+ onEthAutoChanged(m_ui.ethGatewayAuto, m_ui.ethGatewayAuto->checkState(), m_ui.ethGatewayAddr, "DEV9/Eth", "AutoGateway");
+ connect(m_ui.ethGatewayAuto, QOverload::of(&QCheckBox::stateChanged), this, [&](int state) { onEthAutoChanged(m_ui.ethGatewayAuto, state, m_ui.ethGatewayAddr, "DEV9/Eth", "AutoGateway"); });
+
+ SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.ethDNS1Mode, "DEV9/Eth", "ModeDNS1",
+ s_dns_name, Pcsx2Config::DEV9Options::DnsModeNames, Pcsx2Config::DEV9Options::DnsModeNames[static_cast(Pcsx2Config::DEV9Options::DnsMode::Auto)]);
+ onEthDNSModeChanged(m_ui.ethDNS1Mode, m_ui.ethDNS1Mode->currentIndex(), m_ui.ethDNS1Addr, "DEV9/Eth", "ModeDNS1");
+ connect(m_ui.ethDNS1Mode, QOverload::of(&QComboBox::currentIndexChanged), this, [&](int index) { onEthDNSModeChanged(m_ui.ethDNS1Mode, index, m_ui.ethDNS1Addr, "DEV9/Eth", "ModeDNS1"); });
+
+ SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.ethDNS2Mode, "DEV9/Eth", "ModeDNS2",
+ s_dns_name, Pcsx2Config::DEV9Options::DnsModeNames, Pcsx2Config::DEV9Options::DnsModeNames[static_cast(Pcsx2Config::DEV9Options::DnsMode::Auto)]);
+ onEthDNSModeChanged(m_ui.ethDNS2Mode, m_ui.ethDNS2Mode->currentIndex(), m_ui.ethDNS2Addr, "DEV9/Eth", "ModeDNS2");
+ connect(m_ui.ethDNS2Mode, QOverload::of(&QComboBox::currentIndexChanged), this, [&](int index) { onEthDNSModeChanged(m_ui.ethDNS2Mode, index, m_ui.ethDNS2Addr, "DEV9/Eth", "ModeDNS2"); });
+
+ //////////////////////////////////////////////////////////////////////////
+ // DNS Settings
+ //////////////////////////////////////////////////////////////////////////
+ m_ethHost_model = new QStandardItemModel(0, 4, m_ui.ethHosts);
+
+ QStringList headers;
+ headers.push_back(tr("Name"));
+ headers.push_back(tr("Url"));
+ headers.push_back(tr("Address"));
+ headers.push_back(tr("Enabled"));
+ m_ethHost_model->setHorizontalHeaderLabels(headers);
+
+ connect(m_ethHost_model, QOverload::of(&QStandardItemModel::itemChanged), this, &DEV9SettingsWidget::onEthHostEdit);
+
+ m_ethHosts_proxy = new QSortFilterProxyModel(m_ui.ethHosts);
+ m_ethHosts_proxy->setSourceModel(m_ethHost_model);
+
+ m_ui.ethHosts->setModel(m_ethHosts_proxy);
+ m_ui.ethHosts->setItemDelegateForColumn(2, new IPItemDelegate(m_ui.ethHosts));
+
+ RefreshHostList();
+
+ m_ui.ethHosts->installEventFilter(this);
+
+ connect(m_ui.ethHostAdd, &QPushButton::clicked, this, &DEV9SettingsWidget::onEthHostAdd);
+ connect(m_ui.ethHostDel, &QPushButton::clicked, this, &DEV9SettingsWidget::onEthHostDel);
+
+ if (m_dialog->isPerGameSettings())
+ m_ui.ethTabWidget->setTabEnabled(1, false);
+
+ //////////////////////////////////////////////////////////////////////////
+ // HDD Settings
+ //////////////////////////////////////////////////////////////////////////
+ connect(m_ui.hddEnabled, QOverload::of(&QCheckBox::stateChanged), this, &DEV9SettingsWidget::onHddEnabledChanged);
+ SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.hddEnabled, "DEV9/Hdd", "HddEnable", false);
+
+ connect(m_ui.hddFile, &QLineEdit::editingFinished, this, &DEV9SettingsWidget::onHddFileEdit);
+ SettingWidgetBinder::BindWidgetToStringSetting(sif, m_ui.hddFile, "DEV9/Hdd", "HddFile", "DEV9hdd.raw");
+ if (m_dialog->isPerGameSettings())
+ m_ui.hddFile->setPlaceholderText(QString::fromUtf8(QtHost::GetBaseStringSettingValue("DEV9/Hdd", "HddFile", "DEV9hdd.raw")));
+ connect(m_ui.hddBrowseFile, &QPushButton::clicked, this, &DEV9SettingsWidget::onHddBrowseFileClicked);
+
+ //TODO: need a getUintValue for if 48bit support occurs
+ const int size = (u64)m_dialog->getIntValue("DEV9/Hdd", "HddSizeSectors", 0).value() * 512 / (1024 * 1024 * 1024);
+
+ if (m_dialog->isPerGameSettings())
+ {
+ const int sizeGlobal = (u64)QtHost::GetBaseIntSettingValue("DEV9/Hdd", "HddSizeSectors", 0) * 512 / (1024 * 1024 * 1024);
+ m_ui.hddSizeSpinBox->setMinimum(39);
+ m_ui.hddSizeSpinBox->setSpecialValueText(tr("Global [%1]").arg(sizeGlobal));
+ }
+
+ // clang-format off
+ m_ui.hddSizeSlider ->setValue(size);
+ m_ui.hddSizeSpinBox->setValue(size);
+
+ connect(m_ui.hddSizeSlider, QOverload::of(&QSlider ::valueChanged), this, &DEV9SettingsWidget::onHddSizeSlide);
+ connect(m_ui.hddSizeSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &DEV9SettingsWidget::onHddSizeSpin );
+ // clang-format on
+
+ connect(m_ui.hddCreate, &QPushButton::clicked, this, &DEV9SettingsWidget::onHddCreateClicked);
+}
+
+void DEV9SettingsWidget::onEthEnabledChanged(int state)
+{
+ const bool enabled = state == Qt::CheckState::PartiallyChecked ? QtHost::GetBaseBoolSettingValue("DEV9/Eth", "EthEnable", false) : state;
+
+ m_ui.ethDevType->setEnabled(enabled);
+ m_ui.ethDevTypeLabel->setEnabled(enabled);
+ m_ui.ethDevLabel->setEnabled(enabled);
+ m_ui.ethDev->setEnabled(enabled);
+ m_ui.ethTabWidget->setEnabled(enabled);
+}
+
+void DEV9SettingsWidget::onEthDeviceTypeChanged(int index)
+{
+ {
+ QSignalBlocker sb(m_ui.ethDev);
+ m_ui.ethDev->clear();
+ }
+
+ if (index > 0)
+ {
+ std::vector list = m_adapter_list[static_cast(m_api_list[index])];
+
+ const std::string value = m_dialog->getEffectiveStringValue("DEV9/Eth", "EthDevice", "");
+ for (size_t i = 0; i < list.size(); i++)
+ {
+ m_ui.ethDev->addItem(QString::fromUtf8(list[i].name));
+ if (list[i].guid == value)
+ m_ui.ethDev->setCurrentIndex(i);
+ }
+ }
+
+ if (m_dialog->isPerGameSettings())
+ {
+ if (index == 0)
+ {
+ std::vector list = m_adapter_list[static_cast(m_api_list[index])];
+ m_ui.ethDev->addItem(tr("Use Global Setting [%1]").arg(QString::fromUtf8(list[0].name)));
+ m_ui.ethDev->setCurrentIndex(0);
+ m_ui.ethDev->setEnabled(false);
+ }
+ else
+ m_ui.ethDev->setEnabled(true);
+ }
+}
+
+void DEV9SettingsWidget::onEthDeviceChanged(int index)
+{
+ if (index > 0)
+ {
+ const AdapterEntry& adapter = m_adapter_list[static_cast(m_api_list[m_ui.ethDevType->currentIndex()])][index];
+
+ m_dialog->setStringSettingValue("DEV9/Eth", "EthApi", Pcsx2Config::DEV9Options::NetApiNames[static_cast(adapter.type)]);
+ m_dialog->setStringSettingValue("DEV9/Eth", "EthDevice", adapter.guid.c_str());
+ }
+ else if (m_dialog->isPerGameSettings() && m_ui.ethDevType->currentIndex() == 0 && index == 0)
+ {
+ m_dialog->setStringSettingValue("DEV9/Eth", "EthApi", std::nullopt);
+ m_dialog->setStringSettingValue("DEV9/Eth", "EthDevice", std::nullopt);
+ }
+}
+
+void DEV9SettingsWidget::onEthDHCPInterceptChanged(int state)
+{
+ const bool enabled = state == Qt::CheckState::PartiallyChecked ? QtHost::GetBaseBoolSettingValue("DEV9/Eth", "InterceptDHCP", false) : state;
+
+ m_ui.ethPS2Addr->setEnabled(enabled);
+ m_ui.ethPS2AddrLabel->setEnabled(enabled);
+
+ m_ui.ethNetMaskLabel->setEnabled(enabled);
+ m_ui.ethNetMaskAuto->setEnabled(enabled);
+ onEthAutoChanged(m_ui.ethNetMaskAuto, m_ui.ethNetMaskAuto->checkState(), m_ui.ethNetMask, "DEV9/Eth", "AutoMask");
+
+ m_ui.ethGatewayAddrLabel->setEnabled(enabled);
+ m_ui.ethGatewayAuto->setEnabled(enabled);
+ onEthAutoChanged(m_ui.ethGatewayAuto, m_ui.ethGatewayAuto->checkState(), m_ui.ethGatewayAddr, "DEV9/Eth", "AutoGateway");
+
+ m_ui.ethDNS1AddrLabel->setEnabled(enabled);
+ m_ui.ethDNS1Mode->setEnabled(enabled);
+ onEthDNSModeChanged(m_ui.ethDNS1Mode, m_ui.ethDNS1Mode->currentIndex(), m_ui.ethDNS1Addr, "DEV9/Eth", "ModeDNS1");
+
+ m_ui.ethDNS2AddrLabel->setEnabled(enabled);
+ m_ui.ethDNS2Mode->setEnabled(enabled);
+ onEthDNSModeChanged(m_ui.ethDNS2Mode, m_ui.ethDNS2Mode->currentIndex(), m_ui.ethDNS2Addr, "DEV9/Eth", "ModeDNS2");
+}
+
+void DEV9SettingsWidget::onEthIPChanged(QLineEdit* sender, const char* section, const char* key)
+{
+ //Alow clearing a per-game ip setting
+ if (sender->text().isEmpty())
+ {
+ if (m_dialog->getStringValue(section, key, std::nullopt).has_value())
+ m_dialog->setStringSettingValue(section, key, std::nullopt);
+ return;
+ }
+
+ //should already be validated
+ u8 bytes[4];
+ std::string inputString = sender->text().toUtf8().constData();
+ sscanf(inputString.c_str(), "%hhu.%hhu.%hhu.%hhu", &bytes[0], &bytes[1], &bytes[2], &bytes[3]);
+
+ std::string neatStr = StringUtil::StdStringFromFormat("%u.%u.%u.%u", bytes[0], bytes[1], bytes[2], bytes[3]);
+
+ sender->setText(QString::fromUtf8(neatStr.c_str()));
+
+ std::string oldval = m_dialog->getStringValue(section, key, "0.0.0.0").value();
+ if (neatStr != oldval)
+ m_dialog->setStringSettingValue(section, key, neatStr.c_str());
+}
+
+void DEV9SettingsWidget::onEthAutoChanged(QCheckBox* sender, int state, QLineEdit* input, const char* section, const char* key)
+{
+ if (sender->isEnabled())
+ {
+ const bool manual = !(state == Qt::CheckState::PartiallyChecked ? QtHost::GetBaseBoolSettingValue(section, key, true) : state);
+ input->setEnabled(manual);
+ }
+ else
+ input->setEnabled(false);
+}
+
+void DEV9SettingsWidget::onEthDNSModeChanged(QComboBox* sender, int index, QLineEdit* input, const char* section, const char* key)
+{
+ if (sender->isEnabled())
+ {
+ if (m_dialog->isPerGameSettings())
+ {
+ if (index == 0)
+ {
+ const std::string value = QtHost::GetBaseStringSettingValue(section, key, Pcsx2Config::DEV9Options::DnsModeNames[static_cast(Pcsx2Config::DEV9Options::DnsMode::Auto)]);
+ for (int i = 0; Pcsx2Config::DEV9Options::DnsModeNames[i] != nullptr; i++)
+ {
+ if (value == Pcsx2Config::DEV9Options::DnsModeNames[i])
+ {
+ index = i;
+ break;
+ }
+ }
+ }
+ else
+ index--;
+ }
+ const bool manual = index == static_cast(Pcsx2Config::DEV9Options::DnsMode::Manual);
+ input->setEnabled(manual);
+ }
+ else
+ input->setEnabled(false);
+}
+
+void DEV9SettingsWidget::onEthHostAdd()
+{
+ HostEntryUi host;
+ host.Desc = "New Host";
+ host.Enabled = false;
+ AddNewHostConfig(host);
+
+ //Select new Item
+ const QModelIndex viewIndex = m_ethHosts_proxy->mapFromSource(m_ethHost_model->index(m_ethHost_model->rowCount() - 1, 1));
+ m_ui.ethHosts->scrollTo(viewIndex, QAbstractItemView::EnsureVisible);
+ m_ui.ethHosts->selectionModel()->setCurrentIndex(viewIndex, QItemSelectionModel::ClearAndSelect);
+}
+
+void DEV9SettingsWidget::onEthHostDel()
+{
+ if (m_ui.ethHosts->selectionModel()->hasSelection())
+ {
+ const QModelIndex selectedIndex = m_ui.ethHosts->selectionModel()->currentIndex();
+ const int modelRow = m_ethHosts_proxy->mapToSource(selectedIndex).row();
+ DeleteHostConfig(modelRow);
+ }
+}
+
+void DEV9SettingsWidget::onEthHostEdit(QStandardItem* item)
+{
+ const int row = item->row();
+ std::string section = "DEV9/Eth/Hosts/Host" + std::to_string(row);
+ switch (item->column())
+ {
+ case 0: //Name
+ m_dialog->setStringSettingValue(section.c_str(), "Desc", item->text().toUtf8().constData());
+ break;
+ case 1: //URL
+ m_dialog->setStringSettingValue(section.c_str(), "Url", item->text().toUtf8().constData());
+ break;
+ case 2: //IP
+ m_dialog->setStringSettingValue(section.c_str(), "Address", item->text().toUtf8().constData());
+ break;
+ case 3: //Enabled
+ m_dialog->setBoolSettingValue(section.c_str(), "Enabled", item->checkState() == Qt::CheckState::Checked);
+ break;
+ default:
+ break;
+ }
+}
+
+void DEV9SettingsWidget::onHddEnabledChanged(int state)
+{
+ const bool enabled = state == Qt::CheckState::PartiallyChecked ? m_dialog->getEffectiveBoolValue("DEV9/Hdd", "HddEnable", false) : state;
+
+ m_ui.hddFile->setEnabled(enabled);
+ m_ui.hddFileLabel->setEnabled(enabled);
+ m_ui.hddBrowseFile->setEnabled(enabled);
+ m_ui.hddSizeLabel->setEnabled(enabled);
+ m_ui.hddSizeSlider->setEnabled(enabled);
+ m_ui.hddSizeMaxLabel->setEnabled(enabled);
+ m_ui.hddSizeMinLabel->setEnabled(enabled);
+ m_ui.hddSizeSpinBox->setEnabled(enabled);
+ m_ui.hddCreate->setEnabled(enabled);
+}
+
+void DEV9SettingsWidget::onHddBrowseFileClicked()
+{
+ QString path =
+ QDir::toNativeSeparators(QFileDialog::getSaveFileName(QtUtils::GetRootWidget(this), tr("HDD Image File"),
+ !m_ui.hddFile->text().isEmpty() ? m_ui.hddFile->text() : "DEV9hdd.raw", tr("HDD (*.raw)"), nullptr,
+ QFileDialog::DontConfirmOverwrite));
+
+ if (path.isEmpty())
+ return;
+
+ m_ui.hddFile->setText(path);
+ m_ui.hddFile->editingFinished();
+}
+
+void DEV9SettingsWidget::onHddFileEdit()
+{
+ //Check if file exists, if so set HddSize to correct value
+ //GHC uses UTF8 on all platforms
+ fs::path hddPath(m_ui.hddFile->text().toUtf8().constData());
+
+ if (hddPath.empty())
+ return;
+
+ if (hddPath.is_relative())
+ {
+ fs::path path(EmuFolders::Settings.ToString().wx_str());
+ hddPath = path / hddPath;
+ }
+
+ if (!fs::exists(hddPath))
+ return;
+
+ const uintmax_t size = fs::file_size(hddPath);
+
+ const u32 sizeSectors = (size / 512);
+ const int sizeGB = size / 1024 / 1024 / 1024;
+
+ QSignalBlocker sb1(m_ui.hddSizeSpinBox);
+ QSignalBlocker sb2(m_ui.hddSizeSlider);
+ m_ui.hddSizeSpinBox->setValue(sizeGB);
+ m_ui.hddSizeSlider->setValue(sizeGB);
+
+ m_dialog->setIntSettingValue("DEV9/Hdd", "HddSizeSectors", (int)sizeSectors);
+}
+
+void DEV9SettingsWidget::onHddSizeSlide(int i)
+{
+ QSignalBlocker sb(m_ui.hddSizeSpinBox);
+ m_ui.hddSizeSpinBox->setValue(i);
+
+ m_dialog->setIntSettingValue("DEV9/Hdd", "HddSizeSectors", (int)((s64)i * 1024 * 1024 * 1024 / 512));
+}
+
+void DEV9SettingsWidget::onHddSizeSpin(int i)
+{
+ QSignalBlocker sb(m_ui.hddSizeSlider);
+ m_ui.hddSizeSlider->setValue(i);
+
+ //TODO: need a setUintSettingValue for if 48bit support occurs
+ if (i == 39)
+ m_dialog->setIntSettingValue("DEV9/Hdd", "HddSizeSectors", std::nullopt);
+ else
+ m_dialog->setIntSettingValue("DEV9/Hdd", "HddSizeSectors", i * (1024 * 1024 * 1024 / 512));
+}
+
+void DEV9SettingsWidget::onHddCreateClicked()
+{
+ //Do the thing
+ fs::path hddPath(m_ui.hddFile->text().toUtf8().constData());
+
+ u64 sizeBytes = (u64)m_dialog->getEffectiveIntValue("DEV9/Hdd", "HddSizeSectors", 0) * 512;
+ if (sizeBytes == 0 || hddPath.empty())
+ {
+ QMessageBox::warning(this, QObject::tr("HDD Creator"),
+ QObject::tr("Failed to create HDD image"),
+ QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
+ return;
+ }
+
+ if (hddPath.is_relative())
+ {
+ //Note, EmuFolders is still wx strings
+ fs::path path(EmuFolders::Settings.ToString().wx_str());
+ hddPath = path / hddPath;
+ }
+
+ if (fs::exists(hddPath))
+ {
+ //GHC uses UTF8 on all platforms
+ QMessageBox::StandardButton selection =
+ QMessageBox::question(this, tr("Overwrite File?"),
+ tr("HDD image \"%1\" already exists?\n\n"
+ "Do you want to overwrite?")
+ .arg(QString::fromUtf8(hddPath.u8string().c_str())),
+ QMessageBox::Yes | QMessageBox::No);
+ if (selection == QMessageBox::No)
+ return;
+ else
+ fs::remove(hddPath);
+ }
+
+ HddCreateQt hddCreator(this);
+ hddCreator.filePath = hddPath;
+ hddCreator.neededSize = sizeBytes;
+ hddCreator.Start();
+
+ if (!hddCreator.errored)
+ {
+ QMessageBox::information(this, tr("HDD Creator"),
+ tr("HDD image created"),
+ QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
+ }
+}
+
+void DEV9SettingsWidget::showEvent(QShowEvent* event)
+{
+ QWidget::showEvent(event);
+
+ //The API combobox dosn't set the EthApi field, that is performed by the device combobox (in addition to saving the device)
+ //This means that this setting can get out of sync with true value, so revert to that if the ui is closed and opened
+ const std::string value = m_dialog->getStringValue("DEV9/Eth", "EthApi", Pcsx2Config::DEV9Options::NetApiNames[static_cast(Pcsx2Config::DEV9Options::NetApi::Unset)]).value();
+
+ //disconnect temporally to prevent saving a vaule already in the config file
+ disconnect(m_ui.ethDev, QOverload::of(&QComboBox::currentIndexChanged), this, &DEV9SettingsWidget::onEthDeviceChanged);
+ for (int i = 0; m_api_namelist[i] != nullptr; i++)
+ {
+ if (value == m_api_valuelist[i])
+ {
+ m_ui.ethDevType->setCurrentIndex(i);
+ break;
+ }
+ }
+ connect(m_ui.ethDev, QOverload::of(&QComboBox::currentIndexChanged), this, &DEV9SettingsWidget::onEthDeviceChanged);
+}
+
+/*
+ * QtUtils::ResizeColumnsForTableView() needs the widget to already be the correct size
+ * Doing this in our resizeEvent (like GameListWidget does) dosn't work if the ui is
+ * hidden, maybe because our table is nested within group & tab widgets
+ * We could also listern to out show event, but we also need to listern to the tab
+ * changed signal, in the event that another tab is selected when our ui is shown
+ *
+ * Instead, lets use an eventFilter to determine exactly when the host table is shown
+ * However, the eventFilter is ran before the widgets event handler, meaning the table
+ * is still the wrong size, so we also need to check the show event
+ */
+bool DEV9SettingsWidget::eventFilter(QObject* object, QEvent* event)
+{
+ if (object == m_ui.ethHosts)
+ {
+ //Check isVisible to avoind an unnessecery call to ResizeColumnsForTableView()
+ if (event->type() == QEvent::Resize && m_ui.ethHosts->isVisible())
+ QtUtils::ResizeColumnsForTableView(m_ui.ethHosts, {-1, 170, 90, 80});
+ else if (event->type() == QEvent::Show)
+ QtUtils::ResizeColumnsForTableView(m_ui.ethHosts, {-1, 170, 90, 80});
+ }
+ return false;
+}
+
+void DEV9SettingsWidget::AddAdapter(const AdapterEntry& adapter)
+{
+ //divide into seperate adapter lists
+
+ if (std::find(m_api_list.begin(), m_api_list.end(), adapter.type) == m_api_list.end())
+ m_api_list.push_back(adapter.type);
+ const u32 idx = static_cast(adapter.type);
+
+ while (m_adapter_list.size() <= idx)
+ {
+ //Add blank adapter
+ AdapterEntry blankAdapter;
+ blankAdapter.guid = "";
+ blankAdapter.name = "";
+ blankAdapter.type = static_cast(m_adapter_list.size());
+ m_adapter_list.push_back({blankAdapter});
+ }
+
+ m_adapter_list[idx].push_back(adapter);
+}
+
+void DEV9SettingsWidget::RefreshHostList()
+{
+ while (m_ethHost_model->rowCount() > 0)
+ m_ethHost_model->removeRow(0);
+
+ //Load list
+ std::vector hosts;
+
+ const int hostLength = CountHostsConfig();
+ for (int i = 0; i < hostLength; i++)
+ {
+ std::string section = "DEV9/Eth/Hosts/Host" + std::to_string(i);
+
+ HostEntryUi entry;
+ entry.Url = m_dialog->getStringValue(section.c_str(), "Url", "").value();
+ entry.Desc = m_dialog->getStringValue(section.c_str(), "Desc", "").value();
+ entry.Address = m_dialog->getStringValue(section.c_str(), "Address", "").value();
+ entry.Enabled = m_dialog->getBoolValue(section.c_str(), "Enabled", false).value();
+ hosts.push_back(entry);
+ }
+
+ for (int i = 0; i < hostLength; i++)
+ {
+ HostEntryUi entry = hosts[i];
+ const int row = m_ethHost_model->rowCount();
+ m_ethHost_model->insertRow(row);
+
+ QSignalBlocker sb(m_ethHost_model);
+
+ QStandardItem* nameItem = new QStandardItem();
+ nameItem->setText(QString::fromStdString(entry.Desc));
+ m_ethHost_model->setItem(row, 0, nameItem);
+
+ QStandardItem* urlItem = new QStandardItem();
+ urlItem->setText(QString::fromStdString(entry.Url));
+ m_ethHost_model->setItem(row, 1, urlItem);
+
+ QStandardItem* addressItem = new QStandardItem();
+ addressItem->setText(QString::fromStdString(entry.Address));
+ m_ethHost_model->setItem(row, 2, addressItem);
+
+ QStandardItem* enabledItem = new QStandardItem();
+ enabledItem->setEditable(false);
+ enabledItem->setCheckable(true);
+ enabledItem->setCheckState(entry.Enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked);
+ m_ethHost_model->setItem(row, 3, enabledItem);
+ }
+
+ m_ui.ethHosts->sortByColumn(0, Qt::AscendingOrder);
+}
+
+int DEV9SettingsWidget::CountHostsConfig()
+{
+ return m_dialog->getIntValue("DEV9/Eth/Hosts", "Count", 0).value();
+}
+
+void DEV9SettingsWidget::AddNewHostConfig(const HostEntryUi& host)
+{
+ const int hostLength = CountHostsConfig();
+ std::string section = "DEV9/Eth/Hosts/Host" + std::to_string(hostLength);
+ // clang-format off
+ m_dialog->setStringSettingValue(section.c_str(), "Url", host.Url.c_str());
+ m_dialog->setStringSettingValue(section.c_str(), "Desc", host.Desc.c_str());
+ m_dialog->setStringSettingValue(section.c_str(), "Address", host.Address.c_str());
+ m_dialog->setBoolSettingValue (section.c_str(), "Enabled", host.Enabled);
+ // clang-format on
+ m_dialog->setIntSettingValue("DEV9/Eth/Hosts", "Count", hostLength + 1);
+ RefreshHostList();
+}
+
+void DEV9SettingsWidget::DeleteHostConfig(int index)
+{
+ const int hostLength = CountHostsConfig();
+
+ //Shuffle entries down to ovewrite deleted entry
+ for (int i = index; i < hostLength - 1; i++)
+ {
+ std::string section = "DEV9/Eth/Hosts/Host" + std::to_string(i);
+ std::string sectionAhead = "DEV9/Eth/Hosts/Host" + std::to_string(i + 1);
+
+ // clang-format off
+ m_dialog->setStringSettingValue(section.c_str(), "Url", m_dialog->getStringValue(sectionAhead.c_str(), "Url", "").value().c_str());
+ m_dialog->setStringSettingValue(section.c_str(), "Desc", m_dialog->getStringValue(sectionAhead.c_str(), "Desc", "").value().c_str());
+ m_dialog->setStringSettingValue(section.c_str(), "Address", m_dialog->getStringValue(sectionAhead.c_str(), "Address", "0.0.0.0").value().c_str());
+ m_dialog->setBoolSettingValue (section.c_str(), "Enabled", m_dialog->getBoolValue (sectionAhead.c_str(), "Enabled", false).value());
+ // clang-format on
+ }
+
+ //Delete last entry
+ std::string section = "DEV9/Eth/Hosts/Host" + std::to_string(hostLength - 1);
+ //Specifying a value of nullopt will delete the key
+ //if the key is a nullptr, the whole section is deleted
+ m_dialog->setStringSettingValue(section.c_str(), nullptr, std::nullopt);
+
+ m_dialog->setIntSettingValue("DEV9/Eth/Hosts", "Count", hostLength - 1);
+ RefreshHostList();
+}
+
+DEV9SettingsWidget::~DEV9SettingsWidget() = default;
diff --git a/pcsx2-qt/Settings/DEV9SettingsWidget.h b/pcsx2-qt/Settings/DEV9SettingsWidget.h
new file mode 100644
index 0000000000..a6e2683e72
--- /dev/null
+++ b/pcsx2-qt/Settings/DEV9SettingsWidget.h
@@ -0,0 +1,114 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "ui_DEV9SettingsWidget.h"
+
+#include "DEV9/net.h"
+
+class SettingsDialog;
+
+class IPValidator : public QValidator
+{
+ Q_OBJECT
+
+public:
+ explicit IPValidator(QObject* parent = nullptr, bool allowEmpty = false);
+ virtual State validate(QString& input, int& pos) const override;
+
+private:
+ static const QRegularExpression intermediateRegex;
+ static const QRegularExpression finalRegex;
+
+ bool m_allowEmpty;
+};
+
+class IPItemDelegate : public QItemDelegate
+{
+ Q_OBJECT
+
+public:
+ explicit IPItemDelegate(QObject* parent = nullptr);
+
+protected:
+ QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const;
+ void setEditorData(QWidget* editor, const QModelIndex& index) const;
+ void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const;
+ void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const;
+};
+
+class DEV9SettingsWidget : public QWidget
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void onEthEnabledChanged(int state);
+ void onEthDeviceTypeChanged(int index);
+ void onEthDeviceChanged(int index);
+ void onEthDHCPInterceptChanged(int state);
+ void onEthIPChanged(QLineEdit* sender, const char* section, const char* key);
+ void onEthAutoChanged(QCheckBox* sender, int state, QLineEdit* input, const char* section, const char* key);
+ void onEthDNSModeChanged(QComboBox* sender, int index, QLineEdit* input, const char* section, const char* key);
+ void onEthHostAdd();
+ void onEthHostDel();
+ void onEthHostEdit(QStandardItem* item);
+
+ void onHddEnabledChanged(int state);
+ void onHddBrowseFileClicked();
+ void onHddFileEdit();
+ void onHddSizeSlide(int i);
+ void onHddSizeSpin(int i);
+ void onHddCreateClicked();
+
+public:
+ DEV9SettingsWidget(SettingsDialog* dialog, QWidget* parent);
+ ~DEV9SettingsWidget();
+
+protected:
+ void showEvent(QShowEvent* event);
+ bool eventFilter(QObject* object, QEvent* event);
+
+private:
+ struct HostEntryUi
+ {
+ std::string Url;
+ std::string Desc;
+ std::string Address = "0.0.0.0";
+ bool Enabled;
+ };
+
+ void AddAdapter(const AdapterEntry& adapter);
+ void RefreshHostList();
+ int CountHostsConfig();
+ void AddNewHostConfig(const HostEntryUi& host);
+ void DeleteHostConfig(int index);
+
+ SettingsDialog* m_dialog;
+
+ Ui::DEV9SettingsWidget m_ui;
+
+ QStandardItemModel* m_ethHost_model;
+ QSortFilterProxyModel* m_ethHosts_proxy;
+
+ std::vector m_api_list;
+ std::vector m_api_namelist;
+ std::vector m_api_valuelist;
+ std::vector> m_adapter_list;
+};
diff --git a/pcsx2-qt/Settings/DEV9SettingsWidget.ui b/pcsx2-qt/Settings/DEV9SettingsWidget.ui
new file mode 100644
index 0000000000..5dfbaea000
--- /dev/null
+++ b/pcsx2-qt/Settings/DEV9SettingsWidget.ui
@@ -0,0 +1,370 @@
+
+
+ DEV9SettingsWidget
+
+
+
+ 0
+ 0
+ 600
+ 500
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ Ethernet
+
+
+
-
+
+
+ -
+
+
+ Ethernet Device:
+
+
+
+ -
+
+
+ Ethernet Device Type:
+
+
+
+ -
+
+
+ 0
+
+
+ true
+
+
+
+ Intercept DHCP
+
+
+
-
+
+
+ -
+
+
+ Enabled
+
+
+ true
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ Subnet Mask:
+
+
+
+ -
+
+
+ Gateway Address:
+
+
+
+ -
+
+
+ Auto
+
+
+
+ -
+
+
+ Intercept DHCP:
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ Auto
+
+
+
+ -
+
+
+ PS2 Address:
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ DNS1 Address:
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ DNS2 Address:
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ Internal DNS
+
+
+ -
+
+
-
+
+
+ Add
+
+
+
+ -
+
+
+ Delete
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+ -
+
+
+ Internal DNS can be selected using the DNS1/2 dropdowns, or by setting them to 192.0.2.1
+
+
+
+ -
+
+
+ QAbstractItemView::SingleSelection
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+
+
+ -
+
+
+ -
+
+
+ Enabled
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ Hard Disk Drive
+
+
+
-
+
+
+ HDD File:
+
+
+
+ -
+
+
-
+
+
+ 40
+
+
+
+ -
+
+
+ 40
+
+
+ 120
+
+
+ 1
+
+
+ Qt::Horizontal
+
+
+ QSlider::TicksBelow
+
+
+ 5
+
+
+
+ -
+
+
+ 120
+
+
+
+
+
+ -
+
+
+ 40
+
+
+ 120
+
+
+
+ -
+
+
+ HDD Size (GiB):
+
+
+
+ -
+
+
+ Enabled
+
+
+ true
+
+
+
+ -
+
+
+ Browse
+
+
+
+ -
+
+
+ -
+
+
+ Create Image
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 68
+
+
+
+
+
+
+
+
+
diff --git a/pcsx2-qt/Settings/HddCreateQt.cpp b/pcsx2-qt/Settings/HddCreateQt.cpp
new file mode 100644
index 0000000000..75fc81826d
--- /dev/null
+++ b/pcsx2-qt/Settings/HddCreateQt.cpp
@@ -0,0 +1,55 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2020 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#include "PrecompiledHeader.h"
+#include
+#include "HddCreateQt.h"
+
+HddCreateQt::HddCreateQt(QWidget* parent)
+ : m_parent{parent}
+ , progressDialog{nullptr}
+{
+}
+
+void HddCreateQt::Init()
+{
+ reqMiB = (neededSize + ((1024 * 1024) - 1)) / (1024 * 1024);
+
+ progressDialog = new QProgressDialog(QObject::tr("Creating HDD file \n %1 / %2 MiB").arg(0).arg(reqMiB), QObject::tr("Cancel"), 0, reqMiB, m_parent);
+ progressDialog->setWindowTitle("HDD Creator");
+ progressDialog->setWindowModality(Qt::WindowModal);
+}
+
+void HddCreateQt::SetFileProgress(u64 currentSize)
+{
+ const int writtenMB = (currentSize + ((1024 * 1024) - 1)) / (1024 * 1024);
+ progressDialog->setValue(writtenMB);
+ progressDialog->setLabelText(QObject::tr("Creating HDD file \n %1 / %2 MiB").arg(writtenMB).arg(reqMiB));
+
+ if (progressDialog->wasCanceled())
+ SetCanceled();
+}
+
+void HddCreateQt::SetError()
+{
+ QMessageBox::warning(progressDialog, QObject::tr("HDD Creator"),
+ QObject::tr("Failed to create HDD image"),
+ QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
+}
+
+void HddCreateQt::Cleanup()
+{
+ delete progressDialog;
+}
diff --git a/pcsx2-qt/Settings/HddCreateQt.h b/pcsx2-qt/Settings/HddCreateQt.h
new file mode 100644
index 0000000000..cadb645313
--- /dev/null
+++ b/pcsx2-qt/Settings/HddCreateQt.h
@@ -0,0 +1,39 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2020 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#pragma once
+
+#include
+
+#include "DEV9/ATA/HddCreate.h"
+
+class HddCreateQt : public HddCreate
+{
+public:
+ HddCreateQt(QWidget* parent);
+ virtual ~HddCreateQt(){};
+
+private:
+ QWidget* m_parent;
+ QProgressDialog* progressDialog;
+
+ int reqMiB;
+
+protected:
+ virtual void Init();
+ virtual void Cleanup();
+ virtual void SetFileProgress(u64 currentSize);
+ virtual void SetError();
+};
diff --git a/pcsx2-qt/Settings/SettingsDialog.cpp b/pcsx2-qt/Settings/SettingsDialog.cpp
index bd72f410dc..511f801362 100644
--- a/pcsx2-qt/Settings/SettingsDialog.cpp
+++ b/pcsx2-qt/Settings/SettingsDialog.cpp
@@ -33,6 +33,7 @@
#include "GameFixSettingsWidget.h"
#include "GameListSettingsWidget.h"
#include "GraphicsSettingsWidget.h"
+#include "DEV9SettingsWidget.h"
#include "HotkeySettingsWidget.h"
#include "InterfaceSettingsWidget.h"
#include "MemoryCardSettingsWidget.h"
@@ -126,6 +127,10 @@ void SettingsDialog::setupUi(const GameList::Entry* game)
addWidget(m_memory_card_settings = new MemoryCardSettingsWidget(this, m_ui.settingsContainer), tr("Memory Cards"),
QStringLiteral("sd-card-line"), tr("Memory Card Settings
"));
}
+
+ addWidget(m_dev9_settings = new DEV9SettingsWidget(this, m_ui.settingsContainer), tr("Network & HDD"), QStringLiteral("dashboard-line"),
+ tr("Network & HDD Settings
These options control the network connectivity and internal HDD storage of the console.
"
+ "Mouse over an option for additional information."));
m_ui.settingsCategory->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_ui.settingsCategory->setCurrentRow(0);
diff --git a/pcsx2-qt/Settings/SettingsDialog.h b/pcsx2-qt/Settings/SettingsDialog.h
index 497a633941..ce46c5d1cb 100644
--- a/pcsx2-qt/Settings/SettingsDialog.h
+++ b/pcsx2-qt/Settings/SettingsDialog.h
@@ -38,6 +38,7 @@ class GameFixSettingsWidget;
class GraphicsSettingsWidget;
class AudioSettingsWidget;
class MemoryCardSettingsWidget;
+class DEV9SettingsWidget;
class SettingsDialog final : public QDialog
{
@@ -63,6 +64,7 @@ public:
__fi GraphicsSettingsWidget* getGraphicsSettingsWidget() const { return m_graphics_settings; }
__fi AudioSettingsWidget* getAudioSettingsWidget() const { return m_audio_settings; }
__fi MemoryCardSettingsWidget* getMemoryCardSettingsWidget() const { return m_memory_card_settings; }
+ __fi DEV9SettingsWidget* getDEV9SettingsWidget() const { return m_dev9_settings; }
void registerWidgetHelp(QObject* object, QString title, QString recommended_value, QString text);
bool eventFilter(QObject* object, QEvent* event) override;
@@ -98,7 +100,7 @@ protected:
private:
enum : u32
{
- MAX_SETTINGS_WIDGETS = 10
+ MAX_SETTINGS_WIDGETS = 11
};
void setupUi(const GameList::Entry* game);
@@ -119,6 +121,7 @@ private:
GraphicsSettingsWidget* m_graphics_settings = nullptr;
AudioSettingsWidget* m_audio_settings = nullptr;
MemoryCardSettingsWidget* m_memory_card_settings = nullptr;
+ DEV9SettingsWidget* m_dev9_settings = nullptr;
std::array m_category_help_text;
diff --git a/pcsx2-qt/pcsx2-qt.vcxproj b/pcsx2-qt/pcsx2-qt.vcxproj
index eb099fb42b..14e458e6b6 100644
--- a/pcsx2-qt/pcsx2-qt.vcxproj
+++ b/pcsx2-qt/pcsx2-qt.vcxproj
@@ -167,6 +167,8 @@
+
+
@@ -199,6 +201,8 @@
+
+
@@ -239,6 +243,7 @@
+
@@ -306,6 +311,9 @@
Document
+
+ Document
+
Document
diff --git a/pcsx2-qt/pcsx2-qt.vcxproj.filters b/pcsx2-qt/pcsx2-qt.vcxproj.filters
index 3d9270a21c..326f754c78 100644
--- a/pcsx2-qt/pcsx2-qt.vcxproj.filters
+++ b/pcsx2-qt/pcsx2-qt.vcxproj.filters
@@ -185,6 +185,9 @@
moc
+
+ moc
+
Settings
@@ -200,6 +203,12 @@
Settings
+
+ Settings
+
+
+ Settings
+
@@ -211,6 +220,9 @@
+
+ Settings
+
@@ -283,6 +295,9 @@
Settings
+
+ Settings
+
@@ -345,5 +360,8 @@
Settings
+
+ Settings
+
\ No newline at end of file