mirror of
https://github.com/FEX-Emu/FEX.git
synced 2026-10-08 15:00:18 +02:00
911 lines
32 KiB
QML
911 lines
32 KiB
QML
// SPDX-License-Identifier: MIT
|
|
import QtQuick 2.15
|
|
import QtQuick.Controls 2.15
|
|
import QtQuick.Layouts 1.15
|
|
|
|
import FEX.ConfigModel 1.0
|
|
import FEX.HostLibsModel 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 loadedDefaults: 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
|
|
|
|
onConfigDirtyChanged: {
|
|
if (!configDirty) {
|
|
// We either just saved or loaded a file
|
|
loadedDefaults = false
|
|
}
|
|
}
|
|
|
|
function refreshUI() {
|
|
refreshCache = !refreshCache
|
|
}
|
|
|
|
function urlToLocalFile(theurl: url): string {
|
|
var str = theurl.toString()
|
|
if (str.startsWith("file://")) {
|
|
return decodeURIComponent(str.substring(7))
|
|
}
|
|
if (str.startsWith("file:")) {
|
|
return decodeURIComponent(str.substring(5))
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
FileDialog {
|
|
id: openFileDialog
|
|
property bool isSaving: false
|
|
|
|
title: isSaving ? qsTr("Save FEX configuration") : qsTr("Open FEX configuration")
|
|
nameFilters: [ qsTr("Config files(*.json)"), qsTr("All files(*)") ]
|
|
|
|
selectExisting: !isSaving
|
|
|
|
property var onNextAccept: null
|
|
|
|
// Prompts the user for an existing file and calls the callback on completion
|
|
function loadAndThen(callback) {
|
|
isSaving = false
|
|
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 saveAndThen(callback) {
|
|
isSaving = true
|
|
console.assert(!onNextAccept, "Tried to open dialog multiple times")
|
|
onNextAccept = callback
|
|
open()
|
|
}
|
|
|
|
onAccepted: {
|
|
if (!isSaving) {
|
|
root.selectedConfigFile(selectedFile)
|
|
}
|
|
configFilename = selectedFile
|
|
configDirty = false
|
|
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.saveAndThen(() => {
|
|
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.saveAndThen(() => {
|
|
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.loadAndThen(() => {})
|
|
}
|
|
Action {
|
|
text: qsTr("&Save")
|
|
shortcut: StandardKey.Save
|
|
onTriggered: root.save("")
|
|
}
|
|
Action {
|
|
text: qsTr("Save &as...")
|
|
shortcut: StandardKey.SaveAs
|
|
onTriggered: {
|
|
openFileDialog.saveAndThen(() => {
|
|
root.save(configFilename)
|
|
});
|
|
}
|
|
}
|
|
MenuSeparator {}
|
|
Action {
|
|
text: qsTr("&Quit")
|
|
shortcut: StandardKey.Quit
|
|
onTriggered: close()
|
|
}
|
|
}
|
|
}
|
|
|
|
header: TabBar {
|
|
id: tabBar
|
|
currentIndex: 0
|
|
|
|
readonly property int advancedIndex: 4
|
|
|
|
TabButton {
|
|
text: qsTr("General")
|
|
}
|
|
TabButton {
|
|
text: qsTr("Emulation")
|
|
}
|
|
TabButton {
|
|
text: qsTr("CPU")
|
|
}
|
|
TabButton {
|
|
text: qsTr("Libraries")
|
|
}
|
|
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
|
|
|
|
editable: true
|
|
|
|
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.fill: parent
|
|
|
|
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("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 {
|
|
text: qsTr("... for vector instructions")
|
|
tooltip: qsTr("Controls TSO emulation on vector load/store instructions")
|
|
config: "VectorTSOEnabled"
|
|
}
|
|
ConfigCheckBox {
|
|
text: qsTr("... for memcpy instructions")
|
|
tooltip: qsTr("Controls TSO emulation on memcpy/memset instructions")
|
|
config: "MemcpySetTSOEnabled"
|
|
}
|
|
ConfigCheckBox {
|
|
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)
|
|
}
|
|
}
|
|
|
|
ConfigCheckBox {
|
|
topPadding: 4
|
|
text: qsTr("Enable non-tearing split-lock atomics")
|
|
config: "StrictInProcessSplitLocks"
|
|
}
|
|
ConfigCheckBox {
|
|
topPadding: 4
|
|
text: qsTr("Use PE volatile metadata for ARM64EC")
|
|
config: "VolatileMetadata"
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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"
|
|
}
|
|
}
|
|
|
|
// Libraries settings
|
|
ScrollablePage {
|
|
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("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 {
|
|
id: hostLibsGroupBox
|
|
title: qsTr("Use host library for:")
|
|
width: parent.width - parent.padding * 2
|
|
|
|
ColumnLayout {
|
|
width: hostLibsGroupBox.width - hostLibsGroupBox.padding * 2
|
|
ScrollView {
|
|
Layout.fillWidth: true
|
|
Layout.maximumHeight: 200
|
|
clip: true
|
|
|
|
Column {
|
|
property string selectedItem
|
|
property string explicitEntry
|
|
|
|
spacing: 4
|
|
|
|
Component.onCompleted: {
|
|
// root.refreshCacheChanged.connect(initState)
|
|
}
|
|
|
|
Repeater {
|
|
model: HostLibsModel
|
|
delegate: CheckBox {
|
|
text: model.display
|
|
visible: text !== "fex_thunk_test" // Hide test library
|
|
checked: (root.refreshCache, model.checked)
|
|
onToggled: {
|
|
configDirty = true
|
|
model.checked = checked
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 === tabBar.advancedIndex ? 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: loadedDefaults
|
|
? qsTr("Config.json not found — loaded defaults")
|
|
: qsTr("Editing %1").arg(urlToLocalFile(configFilename))
|
|
}
|
|
}
|
|
}
|
|
}
|