Merge pull request #3982 from neobrain/feature_fexqonfic

Add Qt-based config editor
This commit is contained in:
Tony Wasserka 2024-08-30 10:43:07 +02:00 committed by GitHub
commit 3020a0db2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1495 additions and 5 deletions

View File

@ -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/)

View File

@ -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/)

View 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)

View 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();
}

View 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&);
};

View 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))
}
}
}
}

View 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>

View 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>

View 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
}

View 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
}

View 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
}
}
}
}

View 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
}

View File

@ -0,0 +1,4 @@
import QtQuick.Dialogs as FromQt
FromQt.FolderDialog {
}

View 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
}