mirror of
https://github.com/FEX-Emu/FEX.git
synced 2024-11-23 06:30:01 +00:00
Merge pull request #3982 from neobrain/feature_fexqonfic
Add Qt-based config editor
This commit is contained in:
commit
3020a0db2b
@ -7,7 +7,7 @@ CHECK_INCLUDE_FILES ("gdb/jit-reader.h" HAVE_GDB_JIT_READER_H)
|
||||
option(BUILD_TESTS "Build unit tests to ensure sanity" TRUE)
|
||||
option(BUILD_FEX_LINUX_TESTS "Build FEXLinuxTests, requires x86 compiler" FALSE)
|
||||
option(BUILD_THUNKS "Build thunks" FALSE)
|
||||
option(BUILD_FEXCONFIG "Build FEXConfig, requires SDL2 and X11" TRUE)
|
||||
set(USE_FEXCONFIG_TOOLKIT "imgui" CACHE STRING "If set, build FEXConfig (qt or imgui)")
|
||||
option(ENABLE_CLANG_THUNKS "Build thunks with clang" FALSE)
|
||||
option(ENABLE_IWYU "Enables include what you use program" FALSE)
|
||||
option(ENABLE_LTO "Enable LTO with compilation" TRUE)
|
||||
@ -304,8 +304,10 @@ endif()
|
||||
set(FMT_INSTALL OFF)
|
||||
add_subdirectory(External/fmt/)
|
||||
|
||||
add_subdirectory(External/imgui/)
|
||||
include_directories(External/imgui/)
|
||||
if (USE_FEXCONFIG_TOOLKIT STREQUAL "imgui")
|
||||
add_subdirectory(External/imgui/)
|
||||
include_directories(External/imgui/)
|
||||
endif()
|
||||
|
||||
add_subdirectory(External/tiny-json/)
|
||||
include_directories(External/tiny-json/)
|
||||
|
@ -1,10 +1,16 @@
|
||||
add_subdirectory(CommonTools)
|
||||
|
||||
if (NOT MINGW_BUILD)
|
||||
if (BUILD_FEXCONFIG)
|
||||
if (USE_FEXCONFIG_TOOLKIT STREQUAL "imgui")
|
||||
add_subdirectory(FEXConfig/)
|
||||
endif()
|
||||
elseif (USE_FEXCONFIG_TOOLKIT STREQUAL "qt")
|
||||
find_package(Qt6 COMPONENTS Qml Quick Widgets)
|
||||
if (NOT Qt6_FOUND)
|
||||
find_package(Qt5 COMPONENTS Qml Quick Widgets REQUIRED)
|
||||
endif()
|
||||
|
||||
add_subdirectory(FEXQonfig/)
|
||||
endif()
|
||||
|
||||
if (ENABLE_GDB_SYMBOLS)
|
||||
add_subdirectory(FEXGDBReader/)
|
||||
|
28
Source/Tools/FEXQonfig/CMakeLists.txt
Normal file
28
Source/Tools/FEXQonfig/CMakeLists.txt
Normal file
@ -0,0 +1,28 @@
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
add_executable(FEXConfig)
|
||||
target_sources(FEXConfig PRIVATE Main.cpp Main.h)
|
||||
target_include_directories(FEXConfig PRIVATE ${CMAKE_SOURCE_DIR}/Source/)
|
||||
target_link_libraries(FEXConfig PRIVATE Common)
|
||||
if (Qt6_FOUND)
|
||||
qt_add_resources(QT_RESOURCES qml6.qrc)
|
||||
target_link_libraries(FEXConfig PRIVATE Qt6::Qml Qt6::Quick Qt6::Widgets)
|
||||
else()
|
||||
qt_add_resources(QT_RESOURCES qml5.qrc)
|
||||
target_link_libraries(FEXConfig PRIVATE Qt5::Qml Qt5::Quick Qt5::Widgets)
|
||||
endif()
|
||||
target_sources(FEXConfig PRIVATE ${QT_RESOURCES})
|
||||
|
||||
if (CMAKE_BUILD_TYPE MATCHES "RELEASE")
|
||||
target_link_options(FEXConfig
|
||||
PRIVATE
|
||||
"LINKER:--gc-sections"
|
||||
"LINKER:--strip-all"
|
||||
"LINKER:--as-needed"
|
||||
)
|
||||
endif()
|
||||
|
||||
install(TARGETS FEXConfig
|
||||
RUNTIME
|
||||
DESTINATION bin
|
||||
COMPONENT runtime)
|
368
Source/Tools/FEXQonfig/Main.cpp
Normal file
368
Source/Tools/FEXQonfig/Main.cpp
Normal file
@ -0,0 +1,368 @@
|
||||
#include "Main.h"
|
||||
|
||||
#include <Common/Config.h>
|
||||
#include <Common/FileFormatCheck.h>
|
||||
#include <FEXCore/fextl/memory.h>
|
||||
#include <FEXCore/fextl/map.h>
|
||||
#include <FEXCore/Config/Config.h>
|
||||
#include <FEXHeaderUtils/Filesystem.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQuickWindow>
|
||||
|
||||
#include <sys/inotify.h>
|
||||
|
||||
namespace fextl {
|
||||
// Helper to convert a std::filesystem::path to a fextl::string.
|
||||
inline fextl::string string_from_path(const std::filesystem::path& Path) {
|
||||
return Path.string().c_str();
|
||||
}
|
||||
} // namespace fextl
|
||||
|
||||
static fextl::unique_ptr<FEXCore::Config::Layer> LoadedConfig {};
|
||||
static fextl::map<FEXCore::Config::ConfigOption, std::pair<std::string, std::string_view>> ConfigToNameLookup;
|
||||
static fextl::map<std::string, FEXCore::Config::ConfigOption> NameToConfigLookup;
|
||||
|
||||
ConfigModel::ConfigModel() {
|
||||
setItemRoleNames(QHash<int, QByteArray> {{Qt::DisplayRole, "display"}, {Qt::UserRole + 1, "optionType"}, {Qt::UserRole + 2, "optionValue"}});
|
||||
Reload();
|
||||
}
|
||||
|
||||
void ConfigModel::Reload() {
|
||||
auto Options = LoadedConfig->GetOptionMap();
|
||||
|
||||
beginResetModel();
|
||||
removeRows(0, rowCount());
|
||||
for (auto& Option : Options) {
|
||||
if (!LoadedConfig->OptionExists(Option.first)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto& [Name, TypeId] = ConfigToNameLookup.find(Option.first)->second;
|
||||
auto Item = new QStandardItem(QString::fromStdString(Name));
|
||||
|
||||
const char* OptionType = TypeId.data();
|
||||
Item->setData(OptionType, Qt::UserRole + 1);
|
||||
Item->setData(QString::fromStdString(Option.second.front().c_str()), Qt::UserRole + 2);
|
||||
appendRow(Item);
|
||||
}
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
bool ConfigModel::has(const QString& Name, bool) const {
|
||||
auto Options = LoadedConfig->GetOptionMap();
|
||||
return LoadedConfig->OptionExists(NameToConfigLookup.at(Name.toStdString()));
|
||||
}
|
||||
|
||||
void ConfigModel::erase(const QString& Name) {
|
||||
assert(has(Name, false));
|
||||
auto Options = LoadedConfig->GetOptionMap();
|
||||
LoadedConfig->Erase(NameToConfigLookup.at(Name.toStdString()));
|
||||
Reload();
|
||||
}
|
||||
|
||||
bool ConfigModel::getBool(const QString& Name, bool) const {
|
||||
auto Options = LoadedConfig->GetOptionMap();
|
||||
|
||||
auto ret = LoadedConfig->Get(NameToConfigLookup.at(Name.toStdString()));
|
||||
if (!ret || !*ret) {
|
||||
throw std::runtime_error("Could not find setting");
|
||||
}
|
||||
return **ret == "1";
|
||||
}
|
||||
|
||||
void ConfigModel::setBool(const QString& Name, bool Value) {
|
||||
auto Options = LoadedConfig->GetOptionMap();
|
||||
LoadedConfig->EraseSet(NameToConfigLookup.at(Name.toStdString()), Value ? "1" : "0");
|
||||
Reload();
|
||||
}
|
||||
|
||||
void ConfigModel::setString(const QString& Name, const QString& Value) {
|
||||
auto Options = LoadedConfig->GetOptionMap();
|
||||
LoadedConfig->EraseSet(NameToConfigLookup.at(Name.toStdString()), Value.toStdString());
|
||||
Reload();
|
||||
}
|
||||
|
||||
void ConfigModel::setStringList(const QString& Name, const QStringList& Values) {
|
||||
auto Options = LoadedConfig->GetOptionMap();
|
||||
|
||||
const auto& Option = NameToConfigLookup.at(Name.toStdString());
|
||||
LoadedConfig->Erase(Option);
|
||||
for (auto& Value : Values) {
|
||||
LoadedConfig->Set(Option, Value.toStdString().c_str());
|
||||
}
|
||||
Reload();
|
||||
}
|
||||
|
||||
void ConfigModel::setInt(const QString& Name, int Value) {
|
||||
auto Options = LoadedConfig->GetOptionMap();
|
||||
LoadedConfig->EraseSet(NameToConfigLookup.at(Name.toStdString()), std::to_string(Value));
|
||||
Reload();
|
||||
}
|
||||
|
||||
QString ConfigModel::getString(const QString& Name, bool) const {
|
||||
auto Options = LoadedConfig->GetOptionMap();
|
||||
|
||||
auto ret = LoadedConfig->Get(NameToConfigLookup.at(Name.toStdString()));
|
||||
if (!ret || !*ret) {
|
||||
throw std::runtime_error("Could not find setting");
|
||||
}
|
||||
return QString::fromUtf8((*ret)->c_str());
|
||||
}
|
||||
|
||||
QStringList ConfigModel::getStringList(const QString& Name, bool) const {
|
||||
auto Options = LoadedConfig->GetOptionMap();
|
||||
|
||||
auto Values = LoadedConfig->All(NameToConfigLookup.at(Name.toStdString()));
|
||||
if (!Values || !*Values) {
|
||||
return {};
|
||||
}
|
||||
QStringList Ret;
|
||||
for (auto& Value : **Values) {
|
||||
Ret.append(Value.c_str());
|
||||
}
|
||||
return Ret;
|
||||
}
|
||||
|
||||
int ConfigModel::getInt(const QString& Name, bool) const {
|
||||
auto Options = LoadedConfig->GetOptionMap();
|
||||
|
||||
auto ret = LoadedConfig->Get(NameToConfigLookup.at(Name.toStdString()));
|
||||
if (!ret || !*ret) {
|
||||
throw std::runtime_error("Could not find setting");
|
||||
}
|
||||
int value;
|
||||
auto res = std::from_chars(&*(*ret)->begin(), &*(*ret)->end(), value);
|
||||
if (res.ptr != &*(*ret)->end()) {
|
||||
throw std::runtime_error("Could not parse integer");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static void LoadDefaultSettings() {
|
||||
LoadedConfig = fextl::make_unique<FEX::Config::EmptyMapper>();
|
||||
#define OPT_BASE(type, group, enum, json, default) LoadedConfig->Set(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(default));
|
||||
#define OPT_STR(group, enum, json, default) LoadedConfig->Set(FEXCore::Config::ConfigOption::CONFIG_##enum, default);
|
||||
#define OPT_STRARRAY(group, enum, json, default) // Do nothing
|
||||
#define OPT_STRENUM(group, enum, json, default) \
|
||||
LoadedConfig->Set(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(FEXCore::ToUnderlying(default)));
|
||||
#include <FEXCore/Config/ConfigValues.inl>
|
||||
|
||||
// Erase unnamed options which shouldn't be set
|
||||
LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS_INTERPRETER);
|
||||
LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_INTERPRETER_INSTALLED);
|
||||
LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_FILENAME);
|
||||
LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_CONFIG_NAME);
|
||||
LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS64BIT_MODE);
|
||||
}
|
||||
|
||||
static void ConfigInit(fextl::string ConfigFilename) {
|
||||
#define OPT_BASE(type, group, enum, json, default) \
|
||||
ConfigToNameLookup[FEXCore::Config::ConfigOption::CONFIG_##enum].first = #json; \
|
||||
ConfigToNameLookup[FEXCore::Config::ConfigOption::CONFIG_##enum].second = #type; \
|
||||
NameToConfigLookup[#json] = FEXCore::Config::ConfigOption::CONFIG_##enum;
|
||||
#include <FEXCore/Config/ConfigValues.inl>
|
||||
#undef OPT_BASE
|
||||
|
||||
// Ensure config and RootFS directories exist
|
||||
std::error_code ec {};
|
||||
fextl::string Dirs[] = {FHU::Filesystem::ParentPath(ConfigFilename), FEXCore::Config::GetDataDirectory() + "RootFS/"};
|
||||
for (auto& Dir : Dirs) {
|
||||
bool created = std::filesystem::create_directories(Dir, ec);
|
||||
if (created) {
|
||||
qInfo() << "Created folder" << Dir.c_str();
|
||||
}
|
||||
if (ec) {
|
||||
QMessageBox err(QMessageBox::Critical, "Failed to create directory", QString("Failed to create \"%1\" folder").arg(Dir.c_str()),
|
||||
QMessageBox::Ok);
|
||||
err.exec();
|
||||
std::exit(EXIT_FAILURE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RootFSModel::RootFSModel() {
|
||||
INotifyFD = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
|
||||
|
||||
fextl::string RootFS = FEXCore::Config::GetDataDirectory() + "RootFS/";
|
||||
FolderFD = inotify_add_watch(INotifyFD, RootFS.c_str(), IN_CREATE | IN_DELETE);
|
||||
if (FolderFD != -1) {
|
||||
Thread = std::thread {&RootFSModel::INotifyThreadFunc, this};
|
||||
} else {
|
||||
qWarning() << "Could not set up inotify. RootFS folder won't be monitored for changes.";
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
Reload();
|
||||
}
|
||||
|
||||
RootFSModel::~RootFSModel() {
|
||||
close(INotifyFD);
|
||||
INotifyFD = -1;
|
||||
|
||||
ExitRequest.count_down();
|
||||
Thread.join();
|
||||
}
|
||||
|
||||
void RootFSModel::Reload() {
|
||||
beginResetModel();
|
||||
removeRows(0, rowCount());
|
||||
|
||||
fextl::string RootFS = FEXCore::Config::GetDataDirectory() + "RootFS/";
|
||||
std::vector<QString> NamedRootFS {};
|
||||
for (auto& it : std::filesystem::directory_iterator(RootFS)) {
|
||||
if (it.is_directory()) {
|
||||
NamedRootFS.push_back(QString::fromStdString(it.path().filename()));
|
||||
} else if (it.is_regular_file()) {
|
||||
// If it is a regular file then we need to check if it is a valid archive
|
||||
if (it.path().extension() == ".sqsh" && FEX::FormatCheck::IsSquashFS(fextl::string_from_path(it.path()))) {
|
||||
NamedRootFS.push_back(QString::fromStdString(it.path().filename()));
|
||||
} else if (it.path().extension() == ".ero" && FEX::FormatCheck::IsEroFS(fextl::string_from_path(it.path()))) {
|
||||
NamedRootFS.push_back(QString::fromStdString(it.path().filename()));
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(NamedRootFS.begin(), NamedRootFS.end(), [](const QString& a, const QString& b) { return QString::localeAwareCompare(a, b) < 0; });
|
||||
for (auto& Entry : NamedRootFS) {
|
||||
appendRow(new QStandardItem(Entry));
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
bool RootFSModel::hasItem(const QString& Name) const {
|
||||
return !findItems(Name, Qt::MatchExactly).empty();
|
||||
}
|
||||
|
||||
QUrl RootFSModel::getBaseUrl() const {
|
||||
return QUrl::fromLocalFile(QString::fromStdString(FEXCore::Config::GetDataDirectory().c_str()) + "RootFS/");
|
||||
}
|
||||
|
||||
void RootFSModel::INotifyThreadFunc() {
|
||||
while (!ExitRequest.try_wait()) {
|
||||
constexpr size_t DATA_SIZE = (16 * (sizeof(struct inotify_event) + NAME_MAX + 1));
|
||||
char buf[DATA_SIZE];
|
||||
int Ret {};
|
||||
do {
|
||||
fd_set Set {};
|
||||
FD_ZERO(&Set);
|
||||
FD_SET(INotifyFD, &Set);
|
||||
struct timeval tv {};
|
||||
// 50 ms
|
||||
tv.tv_usec = 50000;
|
||||
Ret = select(INotifyFD + 1, &Set, nullptr, nullptr, &tv);
|
||||
} while (Ret == 0 && INotifyFD != -1);
|
||||
|
||||
if (Ret == -1 || INotifyFD == -1) {
|
||||
// Just return on error
|
||||
return;
|
||||
}
|
||||
|
||||
// Spin through the events, we don't actually care what they are
|
||||
while (read(INotifyFD, buf, DATA_SIZE) > 0)
|
||||
;
|
||||
|
||||
// Queue update to the data model
|
||||
QMetaObject::invokeMethod(this, "Reload");
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true on success
|
||||
static bool OpenFile(fextl::string Filename) {
|
||||
std::error_code ec {};
|
||||
if (!std::filesystem::exists(Filename, ec)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LoadedConfig = FEX::Config::CreateMainLayer(&Filename);
|
||||
LoadedConfig->Load();
|
||||
|
||||
// Load default options and only overwrite only if the option didn't exist
|
||||
#define OPT_BASE(type, group, enum, json, default) \
|
||||
if (!LoadedConfig->OptionExists(FEXCore::Config::ConfigOption::CONFIG_##enum)) { \
|
||||
LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(default)); \
|
||||
}
|
||||
#define OPT_STR(group, enum, json, default) \
|
||||
if (!LoadedConfig->OptionExists(FEXCore::Config::ConfigOption::CONFIG_##enum)) { \
|
||||
LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_##enum, default); \
|
||||
}
|
||||
#define OPT_STRARRAY(group, enum, json, default) // Do nothing
|
||||
#define OPT_STRENUM(group, enum, json, default) \
|
||||
if (!LoadedConfig->OptionExists(FEXCore::Config::ConfigOption::CONFIG_##enum)) { \
|
||||
LoadedConfig->EraseSet(FEXCore::Config::ConfigOption::CONFIG_##enum, std::to_string(FEXCore::ToUnderlying(default))); \
|
||||
}
|
||||
#include <FEXCore/Config/ConfigValues.inl>
|
||||
|
||||
// Erase unnamed options which shouldn't be set
|
||||
LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS_INTERPRETER);
|
||||
LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_INTERPRETER_INSTALLED);
|
||||
LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_FILENAME);
|
||||
LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_APP_CONFIG_NAME);
|
||||
LoadedConfig->Erase(FEXCore::Config::ConfigOption::CONFIG_IS64BIT_MODE);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ConfigRuntime::ConfigRuntime(const QString& ConfigFilename) {
|
||||
qmlRegisterSingletonInstance<ConfigModel>("FEX.ConfigModel", 1, 0, "ConfigModel", &ConfigModelInst);
|
||||
qmlRegisterSingletonInstance<RootFSModel>("FEX.RootFSModel", 1, 0, "RootFSModel", &RootFSList);
|
||||
Engine.load(QUrl("qrc:/main.qml"));
|
||||
|
||||
Window = qobject_cast<QQuickWindow*>(Engine.rootObjects().first());
|
||||
if (!ConfigFilename.isEmpty()) {
|
||||
Window->setProperty("configFilename", QUrl::fromLocalFile(ConfigFilename));
|
||||
} else {
|
||||
Window->setProperty("configDirty", true);
|
||||
}
|
||||
|
||||
ConfigRuntime::connect(Window, SIGNAL(selectedConfigFile(const QUrl&)), this, SLOT(onLoad(const QUrl&)));
|
||||
ConfigRuntime::connect(Window, SIGNAL(triggeredSave(const QUrl&)), this, SLOT(onSave(const QUrl&)));
|
||||
ConfigRuntime::connect(&ConfigModelInst, SIGNAL(modelReset()), Window, SLOT(refreshUI()));
|
||||
}
|
||||
|
||||
void ConfigRuntime::onSave(const QUrl& Filename) {
|
||||
qInfo() << "Saving to" << Filename.toLocalFile().toStdString().c_str();
|
||||
FEX::Config::SaveLayerToJSON(Filename.toLocalFile().toStdString().c_str(), LoadedConfig.get());
|
||||
}
|
||||
|
||||
void ConfigRuntime::onLoad(const QUrl& Filename) {
|
||||
// TODO: Distinguish between "load" and "overlay".
|
||||
// Currently, the new configuration is overlaid on top of the previous one.
|
||||
|
||||
if (!OpenFile(Filename.toLocalFile().toStdString().c_str())) {
|
||||
// This basically never happens because OpenFile performs no actual syntax checks.
|
||||
// Treat as fatal since the UI state wouldn't be consistent after ignoring the error.
|
||||
QMessageBox err(QMessageBox::Critical, tr("Could not load config file"), tr("Failed to load \"%1\"").arg(Filename.toLocalFile()),
|
||||
QMessageBox::Ok);
|
||||
err.exec();
|
||||
QApplication::exit();
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigModelInst.Reload();
|
||||
RootFSList.Reload();
|
||||
|
||||
QMetaObject::invokeMethod(Window, "refreshUI");
|
||||
}
|
||||
|
||||
int main(int Argc, char** Argv) {
|
||||
QApplication App(Argc, Argv);
|
||||
|
||||
FEX::Config::InitializeConfigs();
|
||||
fextl::string ConfigFilename = Argc > 1 ? Argv[1] : FEXCore::Config::GetConfigFileLocation();
|
||||
ConfigInit(ConfigFilename);
|
||||
|
||||
qInfo() << "Opening" << ConfigFilename.c_str();
|
||||
if (!OpenFile(ConfigFilename)) {
|
||||
// Load defaults if not found
|
||||
ConfigFilename.clear();
|
||||
LoadDefaultSettings();
|
||||
}
|
||||
|
||||
ConfigRuntime Runtime(ConfigFilename.c_str());
|
||||
|
||||
return App.exec();
|
||||
}
|
75
Source/Tools/FEXQonfig/Main.h
Normal file
75
Source/Tools/FEXQonfig/Main.h
Normal file
@ -0,0 +1,75 @@
|
||||
#include <QStandardItemModel>
|
||||
#include <QQmlApplicationEngine>
|
||||
|
||||
#include <latch>
|
||||
#include <thread>
|
||||
|
||||
class QQuickWindow;
|
||||
|
||||
class ConfigModel : public QStandardItemModel {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
public:
|
||||
ConfigModel();
|
||||
|
||||
void Reload();
|
||||
|
||||
public slots:
|
||||
bool has(const QString&, bool unused) const;
|
||||
void erase(const QString&);
|
||||
|
||||
bool getBool(const QString&, bool unused) const;
|
||||
QString getString(const QString&, bool unused) const;
|
||||
QStringList getStringList(const QString&, bool unused) const;
|
||||
int getInt(const QString&, bool unused) const;
|
||||
|
||||
void setBool(const QString&, bool);
|
||||
void setString(const QString&, const QString&);
|
||||
void setStringList(const QString&, const QStringList&);
|
||||
void setInt(const QString&, int value);
|
||||
};
|
||||
|
||||
class RootFSModel : public QStandardItemModel {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
std::thread Thread;
|
||||
std::latch ExitRequest {1};
|
||||
|
||||
int INotifyFD;
|
||||
int FolderFD;
|
||||
|
||||
void INotifyThreadFunc();
|
||||
|
||||
public:
|
||||
RootFSModel();
|
||||
~RootFSModel();
|
||||
|
||||
public slots:
|
||||
void Reload();
|
||||
|
||||
bool hasItem(const QString&) const;
|
||||
|
||||
QUrl getBaseUrl() const;
|
||||
};
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
class ConfigRuntime : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
QQmlApplicationEngine Engine;
|
||||
QQuickWindow* Window = nullptr;
|
||||
RootFSModel RootFSList;
|
||||
ConfigModel ConfigModelInst;
|
||||
|
||||
public:
|
||||
ConfigRuntime(const QString& ConfigFilename);
|
||||
|
||||
public slots:
|
||||
void onSave(const QUrl&);
|
||||
void onLoad(const QUrl&);
|
||||
};
|
903
Source/Tools/FEXQonfig/main.qml
Normal file
903
Source/Tools/FEXQonfig/main.qml
Normal file
@ -0,0 +1,903 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import FEX.ConfigModel 1.0
|
||||
import FEX.RootFSModel 1.0
|
||||
|
||||
// Qt 6 changed the API of the Dialogs module slightly.
|
||||
// The differences are abstracted away in this import:
|
||||
import "qrc:/dialogs"
|
||||
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
|
||||
visible: true
|
||||
width: 540
|
||||
height: 585
|
||||
minimumWidth: 500
|
||||
minimumHeight: 450
|
||||
title: configDirty ? qsTr("FEX configuration *") : qsTr("FEX configuration")
|
||||
|
||||
property url configFilename
|
||||
|
||||
property bool configDirty: false
|
||||
property bool closeConfirmed: false
|
||||
|
||||
signal selectedConfigFile(name: url)
|
||||
signal triggeredSave(name: url)
|
||||
|
||||
// Property used to force reloading any elements that read ConfigModel
|
||||
property bool refreshCache: false
|
||||
|
||||
function refreshUI() {
|
||||
refreshCache = !refreshCache
|
||||
}
|
||||
|
||||
function urlToLocalFile(theurl: url): string {
|
||||
var str = theurl.toString()
|
||||
if (str.startsWith("file://")) {
|
||||
return decodeURIComponent(str.substring(7))
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: openFileDialog
|
||||
title: qsTr("Open FEX configuration")
|
||||
nameFilters: [ qsTr("Config files(*.json)"), qsTr("All files(*)") ]
|
||||
|
||||
property var onNextAccept: null
|
||||
|
||||
// Prompts the user for an existing file and calls the callback on completion
|
||||
function openAndThen(callback) {
|
||||
this.selectExisting = true
|
||||
console.assert(!onNextAccept, "Tried to open dialog multiple times")
|
||||
onNextAccept = callback
|
||||
open()
|
||||
}
|
||||
|
||||
// Prompts the user for a new or existing file and calls the callback on completion
|
||||
function openNewAndThen(callback) {
|
||||
this.selectExisting = false
|
||||
console.assert(!onNextAccept, "Tried to open dialog multiple times")
|
||||
onNextAccept = callback
|
||||
open()
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
root.selectedConfigFile(selectedFile)
|
||||
configFilename = selectedFile
|
||||
if (onNextAccept) {
|
||||
onNextAccept()
|
||||
onNextAccept = null
|
||||
}
|
||||
}
|
||||
|
||||
onRejected: onNextAccept = null
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: confirmCloseDialog
|
||||
title: qsTr("Save changes")
|
||||
text: configFilename.toString() === "" ? qsTr("Save changes before quitting?") : qsTr("Save changes to %1 before quitting?").arg(urlToLocalFile(configFilename))
|
||||
buttons: buttonSave | buttonDiscard | buttonCancel
|
||||
|
||||
onButtonClicked: (button) => {
|
||||
switch (button) {
|
||||
case buttonSave:
|
||||
if (configFilename.toString() === "") {
|
||||
// Filename not yet set => trigger "Save As" dialog
|
||||
openFileDialog.openNewAndThen(() => {
|
||||
save(configFilename)
|
||||
root.close()
|
||||
});
|
||||
return
|
||||
}
|
||||
save(configFilename)
|
||||
root.close()
|
||||
break
|
||||
|
||||
case buttonDiscard:
|
||||
closeConfirmed = true
|
||||
root.close()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClosing: (close) => {
|
||||
if (configDirty) {
|
||||
close.accepted = closeConfirmed
|
||||
onTriggered: confirmCloseDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
function save(filename: url) {
|
||||
if (filename.toString() === "") {
|
||||
filename = configFilename
|
||||
}
|
||||
|
||||
if (filename.toString() === "") {
|
||||
// Filename not yet set => trigger "Save As" dialog
|
||||
openFileDialog.openNewAndThen(() => {
|
||||
save(configFilename)
|
||||
});
|
||||
return
|
||||
}
|
||||
|
||||
triggeredSave(filename)
|
||||
configDirty = false
|
||||
}
|
||||
|
||||
menuBar: MenuBar {
|
||||
Menu {
|
||||
title: qsTr("&File")
|
||||
Action {
|
||||
text: qsTr("&Open...")
|
||||
shortcut: StandardKey.Open
|
||||
// TODO: Ask to discard pending changes first
|
||||
onTriggered: openFileDialog.openAndThen(() => {})
|
||||
}
|
||||
Action {
|
||||
text: qsTr("&Save")
|
||||
shortcut: StandardKey.Save
|
||||
onTriggered: root.save("")
|
||||
}
|
||||
Action {
|
||||
text: qsTr("Save &as...")
|
||||
shortcut: StandardKey.SaveAs
|
||||
onTriggered: {
|
||||
openFileDialog.openNewAndThen(() => {
|
||||
root.save(configFilename)
|
||||
});
|
||||
}
|
||||
}
|
||||
MenuSeparator {}
|
||||
Action {
|
||||
text: qsTr("&Quit")
|
||||
shortcut: StandardKey.Quit
|
||||
onTriggered: close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header: TabBar {
|
||||
id: tabBar
|
||||
currentIndex: 0
|
||||
|
||||
TabButton {
|
||||
text: qsTr("General")
|
||||
}
|
||||
TabButton {
|
||||
text: qsTr("Emulation")
|
||||
}
|
||||
TabButton {
|
||||
text: qsTr("CPU")
|
||||
}
|
||||
TabButton {
|
||||
text: qsTr("Advanced")
|
||||
}
|
||||
}
|
||||
|
||||
component ConfigCheckBox: CheckBox {
|
||||
property string config
|
||||
property string tooltip
|
||||
property bool invert: false
|
||||
|
||||
ToolTip.visible: (visualFocus || hovered) && tooltip !== ""
|
||||
ToolTip.text: tooltip
|
||||
|
||||
onToggled: {
|
||||
configDirty = true
|
||||
ConfigModel.setBool(config, checked ^ invert)
|
||||
}
|
||||
|
||||
checkState: config === "" ? Qt.PartiallyChecked
|
||||
: !ConfigModel.has(config, refreshCache) ? Qt.PartiallyChecked
|
||||
: (ConfigModel.getBool(config, refreshCache) ^ invert) ? Qt.Checked
|
||||
: Qt.Unchecked
|
||||
}
|
||||
|
||||
component ConfigSpinBox: SpinBox {
|
||||
property string config
|
||||
|
||||
textFromValue: (val) => {
|
||||
if (valueFromConfig === "") {
|
||||
return qsTr("(not set)");
|
||||
}
|
||||
|
||||
return val.toString()
|
||||
}
|
||||
|
||||
onValueModified: {
|
||||
configDirty = true
|
||||
ConfigModel.setInt(config, value)
|
||||
}
|
||||
|
||||
property string valueFromConfig: config === "" ? 0 : ConfigModel.has(config, refreshCache) ? ConfigModel.getInt(config, refreshCache).toString() : ""
|
||||
|
||||
value: valueFromConfig
|
||||
from: 0
|
||||
to: 1 << 30
|
||||
}
|
||||
|
||||
component ConfigTextField: TextField {
|
||||
property string config
|
||||
property bool hasData: config !== "" && ConfigModel.has(config, refreshCache)
|
||||
text: hasData ? ConfigModel.getString(config, refreshCache) : "(none set)"
|
||||
enabled: hasData
|
||||
|
||||
onTextEdited: {
|
||||
configDirty = true
|
||||
ConfigModel.setString(config, text)
|
||||
}
|
||||
}
|
||||
|
||||
component ConfigTextFieldForPath: RowLayout {
|
||||
property string text
|
||||
property string config
|
||||
|
||||
property var dialog: FileDialog {}
|
||||
|
||||
FileDialog { id: fileSelectorDialog }
|
||||
|
||||
Label { text: parent.text }
|
||||
ConfigTextField {
|
||||
Layout.fillWidth: true
|
||||
config: parent.config
|
||||
readOnly: true
|
||||
}
|
||||
|
||||
Button {
|
||||
icon.name: "search"
|
||||
onClicked: dialog.open()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
dialog.accepted.connect(() => {
|
||||
var selectedPath = (dialog instanceof FileDialog ? dialog.selectedFile : dialog.selectedFolder)
|
||||
|
||||
configDirty = true
|
||||
ConfigModel.setString(config, urlToLocalFile(selectedPath))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: tabBar.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
currentIndex: tabBar.currentIndex
|
||||
|
||||
component ScrollablePage: ScrollView {
|
||||
id: outer
|
||||
|
||||
readonly property var visibleScrollbarWidth: ScrollBar.vertical.visible ? ScrollBar.vertical.width : 0
|
||||
|
||||
// Children given by the user will be moved into the inner Column
|
||||
default property alias content: inner.children
|
||||
|
||||
property alias itemSpacing: inner.spacing
|
||||
|
||||
Column {
|
||||
id: inner
|
||||
|
||||
spacing: 8
|
||||
padding: 8
|
||||
|
||||
// This must be explicitly set via the id, since parent doesn't seem to be recognized within Column
|
||||
width: outer.width - outer.visibleScrollbarWidth
|
||||
}
|
||||
}
|
||||
|
||||
// Environment settings
|
||||
ScrollablePage {
|
||||
GroupBox {
|
||||
id: rootfsGroupBox
|
||||
title: qsTr("RootFS:")
|
||||
width: parent.width - parent.padding * 2
|
||||
|
||||
ColumnLayout {
|
||||
width: rootfsGroupBox.width - rootfsGroupBox.padding * 2
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumHeight: 150
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: rootfsList
|
||||
|
||||
property string selectedItem
|
||||
property string explicitEntry
|
||||
|
||||
spacing: 4
|
||||
|
||||
Component.onCompleted: {
|
||||
var initState = (ref) => {
|
||||
selectedItem = ConfigModel.has("RootFS", ref) ? ConfigModel.getString("RootFS", ref) : ""
|
||||
|
||||
// RootFSModel only lists entries in the $FEX_HOME/RootFS/ folder.
|
||||
// If a custom path is selected, add it as a dedicated entry
|
||||
if (selectedItem !== "" && !RootFSModel.hasItem(selectedItem)) {
|
||||
explicitEntry = selectedItem
|
||||
|
||||
// Make visible once needed.
|
||||
// Conversely, if the user selects something else after, keep the old option visible to allow easy undoing
|
||||
fallbackRootfsEntry.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
initState(false)
|
||||
root.refreshCacheChanged.connect(initState)
|
||||
}
|
||||
|
||||
function updateRootFS(fileOrFolder: url) {
|
||||
configDirty = true
|
||||
var base = urlToLocalFile(RootFSModel.getBaseUrl())
|
||||
var file = urlToLocalFile(fileOrFolder)
|
||||
if (file.startsWith(base)) {
|
||||
file = file.substring(base.length)
|
||||
}
|
||||
|
||||
ConfigModel.setString("RootFS", file)
|
||||
refreshUI()
|
||||
}
|
||||
|
||||
component RootFSRadioDelegate: RadioButton {
|
||||
property var name
|
||||
|
||||
text: name
|
||||
checked: rootfsList.selectedItem === name
|
||||
|
||||
onToggled: {
|
||||
configDirty = true;
|
||||
ConfigModel.setString("RootFS", name)
|
||||
}
|
||||
}
|
||||
|
||||
RootFSRadioDelegate {
|
||||
id: fallbackRootfsEntry
|
||||
visible: false
|
||||
name: rootfsList.explicitEntry
|
||||
}
|
||||
Repeater {
|
||||
model: RootFSModel
|
||||
delegate: RootFSRadioDelegate { name: model.display }
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
FileDialog {
|
||||
id: addRootfsFileDialog
|
||||
title: qsTr("Select RootFS file")
|
||||
nameFilters: [ qsTr("SquashFS and EroFS (*.sqsh *.ero)"), qsTr("All files(*)") ]
|
||||
currentFolder: RootFSModel.getBaseUrl()
|
||||
onAccepted: rootfsList.updateRootFS(fileUrl)
|
||||
}
|
||||
|
||||
FolderDialog {
|
||||
id: addRootfsFolderDialog
|
||||
title: qsTr("Select RootFS folder")
|
||||
currentFolder: RootFSModel.getBaseUrl()
|
||||
onAccepted: rootfsList.updateRootFS(selectedFolder)
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Add archive")
|
||||
icon.name: "document-open"
|
||||
onClicked: addRootfsFileDialog.open()
|
||||
}
|
||||
Button {
|
||||
text: qsTr("Add folder")
|
||||
icon.name: "folder"
|
||||
onClicked: addRootfsFolderDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
title: qsTr("Library forwarding:")
|
||||
width: parent.width - parent.padding * 2
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: parent ? parent.left : undefined
|
||||
anchors.right: parent ? parent.right : undefined
|
||||
|
||||
id: libfwdConfig
|
||||
|
||||
property url configDir: (() => {
|
||||
var configPath = urlToLocalFile(configFilename)
|
||||
var slashIndex = configPath.lastIndexOf('/')
|
||||
if (slashIndex === -1) {
|
||||
return ""
|
||||
}
|
||||
return "file://" + configPath.substr(0, slashIndex)
|
||||
})()
|
||||
|
||||
ConfigTextFieldForPath {
|
||||
text: qsTr("Configuration:")
|
||||
config: "ThunkConfig"
|
||||
dialog: FileDialog {
|
||||
title: qsTr("Select library forwarding configuration")
|
||||
nameFilters: [ qsTr("JSON files (*.json)"), qsTr("All files(*)") ]
|
||||
currentFolder: libfwdConfig.configDir
|
||||
}
|
||||
}
|
||||
ConfigTextFieldForPath {
|
||||
text: qsTr("Host library folder:")
|
||||
config: "ThunkHostLibs"
|
||||
dialog: FolderDialog {
|
||||
title: qsTr("Select path for host libraries")
|
||||
currentFolder: libfwdConfig.configDir
|
||||
}
|
||||
}
|
||||
ConfigTextFieldForPath {
|
||||
text: qsTr("Guest library folder:")
|
||||
config: "ThunkGuestLibs"
|
||||
dialog: FolderDialog {
|
||||
title: qsTr("Select path for guest libraries")
|
||||
currentFolder: libfwdConfig.configDir
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
title: qsTr("Logging:")
|
||||
width: parent.width - parent.padding * 2
|
||||
|
||||
label: ConfigCheckBox {
|
||||
id: loggingEnabledCheckBox
|
||||
config: "SilentLog"
|
||||
text: qsTr("Logging:")
|
||||
invert: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
enabled: loggingEnabledCheckBox.checked
|
||||
|
||||
anchors.left: parent ? parent.left : undefined
|
||||
anchors.right: parent ? parent.right : undefined
|
||||
|
||||
RowLayout {
|
||||
Label { text: qsTr("Log to:") }
|
||||
|
||||
ComboBox {
|
||||
id: loggingComboBox
|
||||
property string configValue: ConfigModel.has("OutputLog", refreshCache) ? ConfigModel.getString("OutputLog", refreshCache) : ""
|
||||
|
||||
currentIndex: configValue === "" ? -1 : configValue == "server" ? 0 : configValue == "stderr" ? 1 : configValue == "stdout" ? 2 : 3
|
||||
|
||||
onActivated: {
|
||||
configDirty = true
|
||||
var configNames = [ "server", "stderr", "stdout" ]
|
||||
if (currentIndex != -1 && currentIndex < 3) {
|
||||
ConfigModel.setString("OutputLog", configNames[currentIndex])
|
||||
} else {
|
||||
// Set by text field below
|
||||
}
|
||||
}
|
||||
|
||||
model: ListModel {
|
||||
ListElement { text: "FEXServer" }
|
||||
ListElement { text: "stderr" }
|
||||
ListElement { text: "stdout" }
|
||||
ListElement { text: qsTr("File...") }
|
||||
}
|
||||
}
|
||||
|
||||
ConfigTextFieldForPath {
|
||||
visible: loggingComboBox.currentIndex === 3
|
||||
config: "OutputLog"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emulation settings
|
||||
ScrollablePage {
|
||||
RowLayout {
|
||||
Label { text: qsTr("SMC detection:") }
|
||||
ComboBox {
|
||||
currentIndex: ConfigModel.has("SMCChecks", refreshCache) ? ConfigModel.getInt("SMCChecks", refreshCache) : -1
|
||||
|
||||
onActivated: {
|
||||
configDirty = true
|
||||
ConfigModel.setInt("SMCChecks", currentIndex)
|
||||
}
|
||||
|
||||
model: ListModel {
|
||||
ListElement { text: qsTr("None") }
|
||||
ListElement { text: qsTr("MTrack") }
|
||||
ListElement { text: qsTr("Full") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
title: qsTr("Memory model:")
|
||||
width: parent.width - parent.padding * 2
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: parent ? parent.left : undefined
|
||||
anchors.right: parent ? parent.right : undefined
|
||||
|
||||
ButtonGroup {
|
||||
id: tsoButtonGroup
|
||||
buttons: [tso1, tso2, tso3]
|
||||
// Trying to be too clever here will trigger property binding loops,
|
||||
// so require both TSOEnabled and ParanoidTSO to be listed in the config.
|
||||
// If they are not, the state will be displayed as undetermined.
|
||||
checkedButton: !(ConfigModel.has("TSOEnabled", refreshCache) && (ConfigModel.has("ParanoidTSO", refreshCache))) ? null
|
||||
: ConfigModel.getBool("ParanoidTSO", refreshCache) ? tso3
|
||||
: ConfigModel.getBool("TSOEnabled", refreshCache) ? tso2 : tso1
|
||||
|
||||
property int pendingItemChange: -1
|
||||
|
||||
function onClickedButton(index: int) {
|
||||
pendingItemChange = index;
|
||||
|
||||
configDirty = true;
|
||||
|
||||
var newIndex = pendingItemChange
|
||||
var TSOEnabled = newIndex === 1
|
||||
var ParanoidTSO = newIndex === 2
|
||||
ConfigModel.setBool("ParanoidTSO", ParanoidTSO)
|
||||
ConfigModel.setBool("TSOEnabled", TSOEnabled)
|
||||
|
||||
pendingItemChange = -1;
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (pendingItemChange !== -1) {
|
||||
return;
|
||||
}
|
||||
pendingItemChange = tso1.checked ? 0 : tso2.checked ? 1 : tso3.checked ? 2 : -1;
|
||||
if (pendingItemChange) {
|
||||
// Undetermined state, leave as is
|
||||
return;
|
||||
}
|
||||
|
||||
var newIndex = pendingItemChange
|
||||
var TSOEnabled = newIndex === 1
|
||||
var ParanoidTSO = newIndex === 2
|
||||
ConfigModel.setBool("ParanoidTSO", ParanoidTSO)
|
||||
ConfigModel.setBool("TSOEnabled", TSOEnabled)
|
||||
|
||||
pendingItemChange = -1;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
RadioButton {
|
||||
id: tso1
|
||||
text: qsTr("Inaccurate")
|
||||
onToggled: tsoButtonGroup.onClickedButton(0)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
RadioButton {
|
||||
id: tso2
|
||||
text: qsTr("Accurate (TSO)")
|
||||
onToggled: tsoButtonGroup.onClickedButton(1)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: tso2.checked
|
||||
|
||||
ConfigCheckBox {
|
||||
leftPadding: 24
|
||||
text: qsTr("... for vector instructions")
|
||||
tooltip: qsTr("Controls TSO emulation on vector load/store instructions")
|
||||
config: "VectorTSOEnabled"
|
||||
}
|
||||
ConfigCheckBox {
|
||||
leftPadding: 24
|
||||
text: qsTr("... for memcpy instructions")
|
||||
tooltip: qsTr("Controls TSO emulation on memcpy/memset instructions")
|
||||
config: "MemcpySetTSOEnabled"
|
||||
}
|
||||
ConfigCheckBox {
|
||||
leftPadding: 24
|
||||
text: qsTr("... for unaligned half-barriers")
|
||||
tooltip: qsTr("Controls half-barrier TSO emulation on unaligned load/store instructions")
|
||||
config: "HalfBarrierTSOEnabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RadioButton {
|
||||
id: tso3
|
||||
text: qsTr("Overly accurate (paranoid TSO)")
|
||||
onToggled: tsoButtonGroup.onClickedButton(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component EnvVarList: GroupBox {
|
||||
width: parent.width - parent.padding * 2
|
||||
|
||||
property bool ofHost: false
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: parent ? parent.left : undefined
|
||||
anchors.right: parent ? parent.right : undefined
|
||||
|
||||
spacing: 0
|
||||
|
||||
id: envGroup
|
||||
property var values: ConfigModel.getStringList(ofHost ? "HostEnv" : "Env", refreshCache)
|
||||
|
||||
property int editedIndex: -1
|
||||
Repeater {
|
||||
model: parent.values
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
property bool isEditing: envGroup.editedIndex === index
|
||||
|
||||
ItemDelegate {
|
||||
text: modelData;
|
||||
visible: !parent.isEditing
|
||||
onClicked: envGroup.editedIndex = index
|
||||
|
||||
}
|
||||
TextField {
|
||||
id: envVarEditTextField
|
||||
visible: parent.isEditing;
|
||||
text: modelData
|
||||
|
||||
onEditingFinished: {
|
||||
envGroup.editedIndex = -1
|
||||
if (text === modelData) {
|
||||
return
|
||||
}
|
||||
|
||||
var newValues = envGroup.values
|
||||
newValues[model.index] = text
|
||||
configDirty = true
|
||||
ConfigModel.setStringList(ofHost ? "HostEnv" : "Env", newValues)
|
||||
}
|
||||
}
|
||||
Button {
|
||||
visible: parent.isEditing
|
||||
icon.name: "list-remove"
|
||||
onClicked: {
|
||||
envGroup.editedIndex = -1
|
||||
var newValues = []
|
||||
for (var i = 0; i < envGroup.values.length; ++i) {
|
||||
if (i != index) {
|
||||
newValues.push(envGroup.values[i])
|
||||
}
|
||||
}
|
||||
|
||||
configDirty = true
|
||||
ConfigModel.setStringList(ofHost ? "HostEnv" : "Env", newValues)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
TextField {
|
||||
id: envVarTextField
|
||||
Layout.fillWidth: true
|
||||
|
||||
onAccepted: {
|
||||
var newValues = envGroup.values
|
||||
newValues.push(envVarTextField.text)
|
||||
configDirty = true
|
||||
ConfigModel.setStringList(ofHost ? "HostEnv" : "Env", newValues)
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
Button {
|
||||
icon.name : "list-add"
|
||||
enabled: envVarTextField.text !== ""
|
||||
onClicked: envVarTextField.onAccepted()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EnvVarList {
|
||||
title: qsTr("Guest environment variables:")
|
||||
}
|
||||
|
||||
EnvVarList {
|
||||
title: qsTr("Host environment variables:")
|
||||
ofHost: true
|
||||
}
|
||||
}
|
||||
|
||||
// CPU settings
|
||||
ScrollablePage {
|
||||
ConfigCheckBox {
|
||||
text: qsTr("Multiblock")
|
||||
config: "Multiblock"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label { text: qsTr("Block size:") }
|
||||
ConfigSpinBox {
|
||||
config: "MaxInst"
|
||||
from: 0
|
||||
to: 1 << 30
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
title: qsTr("JIT caches:")
|
||||
width: parent.width - parent.padding * 2
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: parent ? parent.left : undefined
|
||||
anchors.right: parent ? parent.right : undefined
|
||||
|
||||
ConfigCheckBox {
|
||||
text: qsTr("Generate AOT")
|
||||
config: "AOTIRGenerate"
|
||||
}
|
||||
ConfigCheckBox {
|
||||
text: qsTr("Capture AOT")
|
||||
config: "AOTIRCapture"
|
||||
}
|
||||
ConfigCheckBox {
|
||||
text: qsTr("Load AOT")
|
||||
config: "AOTIRLoad"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Label { text: qsTr("Cache object code:") }
|
||||
|
||||
ButtonGroup {
|
||||
buttons: cacheObjCodeRadios.children
|
||||
|
||||
checkedButton: ConfigModel.has("CacheObjectCodeCompilation", refreshCache)
|
||||
? cacheObjCodeRadios.children[ConfigModel.getInt("CacheObjectCodeCompilation", refreshCache)]
|
||||
: null
|
||||
|
||||
onClicked: (button) => {
|
||||
configDirty = true
|
||||
for (var idx in buttons) {
|
||||
if (button === buttons[idx]) {
|
||||
ConfigModel.setInt("CacheObjectCodeCompilation", idx)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: cacheObjCodeRadios
|
||||
RadioButton {
|
||||
text: qsTr("Off")
|
||||
}
|
||||
RadioButton {
|
||||
text: qsTr("Read-only")
|
||||
}
|
||||
RadioButton {
|
||||
text: qsTr("Read & write")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConfigCheckBox {
|
||||
text: qsTr("Reduced x87 precision")
|
||||
config: "X87ReducedPrecision"
|
||||
}
|
||||
|
||||
ConfigCheckBox {
|
||||
text: qsTr("Unsafe local flags optimization")
|
||||
config: "ABILocalFlags"
|
||||
}
|
||||
|
||||
ConfigCheckBox {
|
||||
text: qsTr("Disable JIT optimization passes")
|
||||
config: "O0"
|
||||
}
|
||||
}
|
||||
|
||||
// Advanced settings
|
||||
// NOTE: This is wrapped in a Loader that dynamically instantiates/destroys the page contents whenever the tab is selected.
|
||||
// This avoids costly UI updates for its UI elements.
|
||||
// TODO: Options contained multiple times in JSON aren't listed (neither are they in old FEXConfig though)
|
||||
Loader { sourceComponent: tabBar.currentIndex === 3 ? advancedSettingsPage : null }
|
||||
Component {
|
||||
id: advancedSettingsPage
|
||||
ScrollablePage {
|
||||
itemSpacing: 0
|
||||
Frame {
|
||||
width: parent.width - parent.padding * 2
|
||||
id: frame
|
||||
Column {
|
||||
Repeater {
|
||||
model: ConfigModel
|
||||
delegate: RowLayout {
|
||||
width: frame.width - frame.padding * 2
|
||||
|
||||
Label {
|
||||
id: label
|
||||
text: display
|
||||
}
|
||||
|
||||
ConfigCheckBox {
|
||||
visible: optionType == "bool"
|
||||
config: visible ? label.text : ""
|
||||
}
|
||||
|
||||
ConfigTextField {
|
||||
Layout.fillWidth: true
|
||||
visible: optionType == "fextl::string"
|
||||
config: visible ? label.text : ""
|
||||
}
|
||||
|
||||
ConfigSpinBox {
|
||||
visible: optionType.startsWith("int") || optionType.startsWith("uint")
|
||||
config: visible ? label.text : ""
|
||||
from: 0
|
||||
to: 1 << 30
|
||||
}
|
||||
|
||||
// Spacing
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Button {
|
||||
icon.name: "list-remove"
|
||||
onClicked: {
|
||||
ConfigModel.erase(label.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer: Pane {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
padding: 0
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: 0
|
||||
|
||||
ToolSeparator {
|
||||
Layout.fillWidth: true
|
||||
orientation: Qt.Horizontal
|
||||
|
||||
// Override padding from theme.
|
||||
// Some themes use verticalPadding, others topPadding/bottomPadding, so we set them all.
|
||||
verticalPadding: 0
|
||||
bottomPadding: 0
|
||||
topPadding: 0
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
enabled: false
|
||||
text: configFilename.toString() === ""
|
||||
? qsTr("Config.json not found — loaded defaults")
|
||||
: qsTr("Editing %1").arg(urlToLocalFile(configFilename))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
Source/Tools/FEXQonfig/qml5.qrc
Normal file
10
Source/Tools/FEXQonfig/qml5.qrc
Normal file
@ -0,0 +1,10 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>main.qml</file>
|
||||
</qresource>
|
||||
<qresource prefix="/dialogs">
|
||||
<file alias="FileDialog.qml">qt5/FileDialog.qml</file>
|
||||
<file alias="FolderDialog.qml">qt5/FolderDialog.qml</file>
|
||||
<file alias="MessageDialog.qml">qt5/MessageDialog.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
10
Source/Tools/FEXQonfig/qml6.qrc
Normal file
10
Source/Tools/FEXQonfig/qml6.qrc
Normal file
@ -0,0 +1,10 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>main.qml</file>
|
||||
</qresource>
|
||||
<qresource prefix="/dialogs">
|
||||
<file alias="FileDialog.qml">qt6/FileDialog.qml</file>
|
||||
<file alias="FolderDialog.qml">qt6/FolderDialog.qml</file>
|
||||
<file alias="MessageDialog.qml">qt6/MessageDialog.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
9
Source/Tools/FEXQonfig/qt5/FileDialog.qml
Normal file
9
Source/Tools/FEXQonfig/qt5/FileDialog.qml
Normal file
@ -0,0 +1,9 @@
|
||||
import QtQuick.Dialogs 1.3 as FromQt
|
||||
|
||||
FromQt.FileDialog {
|
||||
property url selectedFile
|
||||
property url currentFolder
|
||||
|
||||
folder: currentFolder
|
||||
onAccepted: selectedFile = fileUrl
|
||||
}
|
12
Source/Tools/FEXQonfig/qt5/FolderDialog.qml
Normal file
12
Source/Tools/FEXQonfig/qt5/FolderDialog.qml
Normal file
@ -0,0 +1,12 @@
|
||||
import QtQuick.Dialogs 1.3 as FromQt
|
||||
|
||||
FromQt.FileDialog {
|
||||
property url currentFolder
|
||||
property url selectedFolder
|
||||
|
||||
folder: currentFolder
|
||||
|
||||
selectFolder: true
|
||||
|
||||
onAccepted: selectedFolder = fileUrl
|
||||
}
|
49
Source/Tools/FEXQonfig/qt5/MessageDialog.qml
Normal file
49
Source/Tools/FEXQonfig/qt5/MessageDialog.qml
Normal file
@ -0,0 +1,49 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Dialogs 1.3 as FromQt
|
||||
|
||||
Item {
|
||||
id: dialogParent
|
||||
property alias text: child.text
|
||||
property alias title: child.title
|
||||
|
||||
readonly property int buttonSave: FromQt.Dialog.Save
|
||||
readonly property int buttonDiscard: FromQt.Dialog.Discard
|
||||
readonly property int buttonCancel: FromQt.Dialog.Cancel
|
||||
|
||||
property int buttons
|
||||
|
||||
signal buttonClicked(button: int)
|
||||
|
||||
property bool pendingResult: false
|
||||
|
||||
function open() {
|
||||
// Workaround for QTBUG-91650, due to which signals may get emitted twice
|
||||
pendingResult = true
|
||||
child.open()
|
||||
}
|
||||
|
||||
FromQt.MessageDialog {
|
||||
id: child
|
||||
|
||||
standardButtons: buttons
|
||||
|
||||
onAccepted: {
|
||||
if (pendingResult) {
|
||||
dialogParent.buttonClicked(buttonSave)
|
||||
pendingResult = false
|
||||
}
|
||||
}
|
||||
onDiscard: {
|
||||
if (pendingResult) {
|
||||
dialogParent.buttonClicked(buttonDiscard)
|
||||
pendingResult = false
|
||||
}
|
||||
}
|
||||
onRejected: {
|
||||
if (pendingResult) {
|
||||
dialogParent.buttonClicked(buttonCancel)
|
||||
pendingResult = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
Source/Tools/FEXQonfig/qt6/FileDialog.qml
Normal file
7
Source/Tools/FEXQonfig/qt6/FileDialog.qml
Normal file
@ -0,0 +1,7 @@
|
||||
import QtQuick.Dialogs as FromQt
|
||||
|
||||
FromQt.FileDialog {
|
||||
property bool selectExisting: true
|
||||
property bool selectMultiple: false
|
||||
fileMode: selectMultiple ? FileDialog.OpenFiles : selectExisting ? FileDialog.OpenFile : FileDialog.SaveFile
|
||||
}
|
4
Source/Tools/FEXQonfig/qt6/FolderDialog.qml
Normal file
4
Source/Tools/FEXQonfig/qt6/FolderDialog.qml
Normal file
@ -0,0 +1,4 @@
|
||||
import QtQuick.Dialogs as FromQt
|
||||
|
||||
FromQt.FolderDialog {
|
||||
}
|
7
Source/Tools/FEXQonfig/qt6/MessageDialog.qml
Normal file
7
Source/Tools/FEXQonfig/qt6/MessageDialog.qml
Normal file
@ -0,0 +1,7 @@
|
||||
import QtQuick.Dialogs as FromQt
|
||||
|
||||
FromQt.MessageDialog {
|
||||
readonly property int buttonSave: MessageDialog.Save
|
||||
readonly property int buttonDiscard: MessageDialog.Discard
|
||||
readonly property int buttonCancel: MessageDialog.Cancel
|
||||
}
|
Loading…
Reference in New Issue
Block a user