mirror of
https://github.com/FEX-Emu/FEX.git
synced 2026-10-09 05:00:16 +02:00
This mode has been broken for a long time because it's mostly untested. Barriers, and backpatching while slow have proven that they work. Maintain the one TSO path, at least until all ARM hardware gains support for x86-TSO memory model mode.
896 lines
31 KiB
QML
896 lines
31 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]
|
|
checkedButton: 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
|
|
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
|
|
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"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|
|
}
|
|
}
|