Redo the Preferences to fit/nav better on Steam Deck

Use Repeater for prefs with hooks
This commit is contained in:
Daniel O'Neill 2025-08-24 04:35:02 -07:00
parent bfaf8f8f7e
commit 1e14743bc2
2 changed files with 278 additions and 398 deletions

View file

@ -61,13 +61,25 @@ Dialog {
}
}
header: TabBar {
id: bar
Repeater {
id: pageRepeater
header: Flickable {
implicitHeight: bar.height
implicitWidth: reviewPage.width
TabButton {
text: modelData['name']
contentHeight: bar.height
contentWidth: bar.width
//boundsBehavior: Flickable.StopAtBounds
TabBar {
id: bar
Repeater {
id: pageRepeater
TabButton {
text: modelData['name']
width: implicitWidth
}
}
}
}
@ -94,97 +106,81 @@ Dialog {
Item {
id: contentStuff
visible: pager.currentIndex === 0
implicitHeight: childrenRect.height + 40
//implicitHeight: childrenRect.height + 40
anchors.fill: parent
Label {
id: labelVortexApiKey
anchors {
top: parent.top
left: parent.left
bottom: textNexusAPI.bottom
margins: 5
}
Flickable {
id: generalFlickable
anchors.fill: parent
anchors.margins: 5
contentWidth: width
contentHeight: generalColumn.height
verticalAlignment: Text.AlignVCenter
text: qsTr('Vortex API Key:')
}
TextField {
id: textNexusAPI
anchors {
top: parent.top
right: parent.right
left: labelVortexApiKey.right
margins: 5
}
}
ColumnLayout {
id: generalColumn
implicitWidth: generalFlickable.width
spacing: 10
Label {
id: labelAboutAPI
anchors {
top: textNexusAPI.bottom
left: parent.left
right: parent.right
margins: 5
}
RowLayout {
Layout.fillWidth: true
spacing: 5
textFormat: Text.RichText
wrapMode: Text.Wrap
text: qsTr(`I don't have a fancy application API key yet because I just whipped this app up to begin with, but if you're willing to take a chance and put your personal NexusMods API key in, here's where you'd do it.<br><br>You can find (or request) your personal API key at <a href="https://www.nexusmods.com/users/myaccount?tab=api">https://www.nexusmods.com/users/myaccount?tab=api</a> down at the bottom.<br><br>You'll also need to set <b>quickmod</b> as your system handler for nxm links.`)
onLinkActivated: function(url) { Qt.openUrlExternally(url); }
}
Label {
id: labelVortexApiKey
verticalAlignment: Text.AlignVCenter
text: qsTr('Vortex API Key:')
}
TextField {
id: textNexusAPI
Layout.fillWidth: true
}
}
MenuSeparator {
id: sep
anchors {
top: labelAboutAPI.bottom
left: parent.left
right: parent.right
margins: 5
}
}
Label {
id: labelAboutAPI
Layout.fillWidth: true
textFormat: Text.RichText
wrapMode: Text.Wrap
Layout.maximumWidth: generalFlickable.width
text: qsTr(`I don't have a fancy application API key yet because I just whipped this app up to begin with, but if you're willing to take a chance and put your personal NexusMods API key in, here's where you'd do it.<br><br>You can find (or request) your personal API key at <a href="https://www.nexusmods.com/users/myaccount?tab=api">https://www.nexusmods.com/users/myaccount?tab=api</a> down at the bottom.<br><br>You'll also need to set <b>quickmod</b> as your system handler for nxm links.`)
onLinkActivated: function(url) { Qt.openUrlExternally(url); }
}
Label {
id: labelInstallMethod
anchors {
top: sep.bottom
left: parent.left
bottom: installMethod.bottom
margins: 5
}
MenuSeparator {
id: sep
Layout.fillWidth: true
}
verticalAlignment: Text.AlignVCenter
text: qsTr('Installation Method:')
}
ComboBox {
id: installMethod
anchors {
left: labelInstallMethod.right
top: sep.bottom
right: parent.right
margins: 5
}
RowLayout {
spacing: 5
Layout.fillWidth: true
model: [ { 'type':'symlink', 'text':'Symbolic Link' }, { 'type':'hardlink', 'text':'Hard Link' }, { 'type':'copy', 'text':'Copy Files' } ]
textRole: 'text'
valueRole: 'type'
}
Label {
id: labelInstallMethod
verticalAlignment: Text.AlignVCenter
text: qsTr('Installation Method:')
}
ComboBox {
id: installMethod
Layout.fillWidth: true
model: [ { 'type':'symlink', 'text':'Symbolic Link' }, { 'type':'hardlink', 'text':'Hard Link' }, { 'type':'copy', 'text':'Copy Files' } ]
textRole: 'text'
valueRole: 'type'
}
}
Label {
anchors {
top: installMethod.bottom
left: parent.left
right: parent.right
margins: 5
}
textFormat: Text.RichText
wrapMode: Text.Wrap
text: qsTr("<ul><li><b>Symbolic Link</b> - Links to the configured mod files are created in your game's installation. Base game data cannot be mucked with. This is the safest option, and very fast.</li>"
+"<li><b>Hard Link</b> - It's like a symbolic link but breaks if the mod files and game directory are on different media. This probably isn't what you want.</li>"
+"<li><b>Copy Files</b> - This is the oldschool method: extract the mod files right into your goshdarn game directory. Technically it's the fastest option, but it isn't really safe."
+"</ul><p><center><i>This isn't actually implemented yet, it will always be 'Copy Files'</i></center></p>")
}
Label {
Layout.fillWidth: true
textFormat: Text.RichText
wrapMode: Text.Wrap
Layout.maximumWidth: generalFlickable.width
text: qsTr("<ul><li><b>Symbolic Link</b> - Links to the configured mod files are created in your game's installation. Base game data cannot be mucked with. This is the safest option, and very fast.</li>"
+"<li><b>Hard Link</b> - It's like a symbolic link but breaks if the mod files and game directory are on different media. This probably isn't what you want.</li>"
+"<li><b>Copy Files</b> - This is the oldschool method: extract the mod files right into your goshdarn game directory. Technically it's the fastest option, but it isn't really safe."
+"</ul><p><center><i>This isn't actually implemented yet, it will always be 'Copy Files'</i></center></p>")
}
} // ColumnLayout
} // Flickable
}
Repeater {
@ -195,319 +191,201 @@ Dialog {
id: gameItem
visible: pager.currentIndex === 1+index
anchors.fill: parent
implicitWidth: 580
implicitHeight: 240
readonly property real rowHeight: 32
//implicitWidth: 580
//implicitHeight: 240
property alias enabled: cbEnabled.checked
property alias modspath: modsPath.text
property alias modstagingpath: modStagingPath.text
property alias gamepath: gamePath.text
property alias vfsPath: vfsPathEntry.text
property alias vfsCreatedPath: vfsCreatedPathEntry.text
property alias userpath: userDataPath.text
property alias dbpath: dbPath.text
property string modspath
property string modstagingpath
property string gamepath
property string vfsPath
property string vfsCreatedPath
property string userpath
property string dbpath
property string steamid: modelData['steamid']
property string gamename: modelData['name']
CheckBox {
id: cbEnabled
text: qsTr('Enabled')
anchors {
top: parent.top
left: parent.left
margins: 10
}
}
Column {
id: columnLabels
spacing: 5
anchors {
top: cbEnabled.bottom
left: parent.left
margins: 10
}
height: childrenRect.height
width: childrenRect.width
Flickable {
id: pageFlickable
anchors.fill: parent
anchors.margins: 5
contentWidth: width
contentHeight: pageColumn.height
Label {
height: gameItem.rowHeight
text: qsTr('Mod Storage Directory:')
enabled: cbEnabled.checked
}
Label {
height: gameItem.rowHeight
text: qsTr('Mod Staging Directory:')
enabled: cbEnabled.checked && installMethod.currentValue === 'copy'
}
Label {
height: gameItem.rowHeight
text: qsTr('Real Game Directory:')
enabled: cbEnabled.checked
}
Label {
height: gameItem.rowHeight
text: qsTr('VFS Mountpoint:')
enabled: cbEnabled.checked
}
Label {
height: gameItem.rowHeight
text: qsTr('VFS Sandbox:')
enabled: cbEnabled.checked
}
Label {
height: gameItem.rowHeight
text: qsTr('User Data Directory:')
enabled: cbEnabled.checked
}
Label {
height: gameItem.rowHeight
text: qsTr('Quickmod Database File:')
}
}
ColumnLayout {
id: pageColumn
width: pageFlickable.width
spacing: 10
Column {
id: columnEdits
spacing: 5
anchors {
left: columnLabels.right
top: cbEnabled.bottom
right: columnButtons.left
margins: 10
}
height: childrenRect.height
TextField {
id: modsPath
enabled: cbEnabled.checked
height: gameItem.rowHeight
width: columnEdits.width
ToolTip.visible: hovered
ToolTip.text: qsTr('This is where the mod archive itself is stashed for safe-keeping.\n\nEg: /DATA/modding/%1/mods').arg(gamename)
}
TextField {
id: modStagingPath
enabled: cbEnabled.checked && installMethod.currentValue === 'copy'
height: gameItem.rowHeight
width: columnEdits.width
ToolTip.visible: hovered
ToolTip.text: qsTr('This is where mods are extracted to and linked to the game from.\n\nEg: /DATA/modding/%1/staging').arg(gamename)
}
TextField {
id: gamePath
enabled: cbEnabled.checked
height: gameItem.rowHeight
width: columnEdits.width
ToolTip.visible: hovered
ToolTip.text: qsTr('Where the game is installed.\n\nEg: /DATA/SteamLibrary/steamapps/common/%1-REAL').arg(gamename)
}
TextField {
id: vfsPathEntry
enabled: cbEnabled.checked
height: gameItem.rowHeight
width: columnEdits.width
ToolTip.visible: hovered
ToolTip.text: qsTr('Where to mount the VFS. Typically, this will be the original GOG or Steam path.\n\nEg: /DATA/SteamLibrary/steamapps/common/%1').arg(steamid)
}
TextField {
id: vfsCreatedPathEntry
enabled: cbEnabled.checked
height: gameItem.rowHeight
width: columnEdits.width
ToolTip.visible: hovered
ToolTip.text: qsTr('Where to place new (or modified) files written to the VFS.\n\nEg: /DATA/modding/%1/created').arg(gamename)
}
TextField {
id: userDataPath
enabled: cbEnabled.checked
height: gameItem.rowHeight
width: columnEdits.width
ToolTip.visible: hovered
ToolTip.text: qsTr('This is the root directory of wherever your data and preferences are stored.\n\nEg: /DATA/SteamLibrary/steamapps/compatdata/%1/pfx/drive_c/users/steamuser').arg(steamid)
}
TextField {
id: dbPath
enabled: cbEnabled.checked
height: gameItem.rowHeight
width: columnEdits.width
ToolTip.visible: hovered
ToolTip.text: qsTr('Path to Quickmod.sqlite database.\n\nEg: /DATA/modding/%1/Quickmod.sqlite').arg(gamename)
}
}
Column {
id: columnButtons
spacing: 5
anchors {
top: cbEnabled.bottom
right: gameItem.right
margins: 10
}
height: childrenRect.height
width: childrenRect.width
Button {
text: qsTr('Browse...')
height: gameItem.rowHeight
enabled: cbEnabled.checked
onClicked: {
modsPathDialogue.currentFolder = 'file://' + modsPath.text;
modsPathDialogue.open();
CheckBox {
id: cbEnabled
text: qsTr('Enabled')
}
ToolTip.visible: hovered
ToolTip.text: qsTr('This is where the mod archive itself is stashed for safe-keeping.\n\nEg: /DATA/modding/%1/mods').arg(gamename)
}
Button {
text: qsTr('Browse...')
height: gameItem.rowHeight
enabled: cbEnabled.checked && installMethod.currentValue === 'copy'
onClicked: {
modStagingPathDialogue.currentFolder = 'file://' + modStagingPath.text;
modStagingPathDialogue.open();
property var confEntries: [
{
'label': qsTr('Mod Storage Directory:'),
'tooltip': qsTr('This is where the mod archive itself is stashed for safe-keeping.\n\nEg: /DATA/modding/%1/mods').arg(gamename),
'dialogue': {
'type': 'directory',
'title': qsTr("Select where to store installed mods..."),
'accepted': function(path) {
gameItem.modspath = (''+path).substring(7);
},
},
'changed': function(newtext) { gameItem.modspath = newtext; },
'getValue': function() { return gameItem.modspath; }
},
{
'label': qsTr('Mod Staging Directory:'),
'tooltip': qsTr('This is where mods are extracted to and linked to the game from.\n\nEg: /DATA/modding/%1/staging').arg(gamename),
'disabled': true,
'dialogue': {
'type': 'directory',
'title': qsTr("Select where to extract mods to..."),
'accepted': function(path) {
gameItem.modstagingpath = (''+path).substring(7);
},
},
'changed': function(newtext) { gameItem.modstagingpath = newtext; },
'getValue': function() { return gameItem.modstagingpath; }
},
{
'label': qsTr('Real Game Directory:'),
'tooltip': qsTr('Where the game is installed.\n\nEg: /DATA/SteamLibrary/steamapps/common/%1-REAL').arg(gamename),
'dialogue': {
'type': 'directory',
'title': qsTr("Select the installed game path..."),
'accepted': function(path) {
gameItem.gamepath = (''+path).substring(7);
},
},
'changed': function(newtext) { gameItem.gamepath = newtext; },
'getValue': function() { return gameItem.gamepath; }
},
{
'label': qsTr('VFS Mountpoint:'),
'tooltip': qsTr('Where to mount the VFS. Typically, this will be the original GOG or Steam path.\n\nEg: /DATA/SteamLibrary/steamapps/common/%1').arg(steamid),
'dialogue': {
'type': 'directory',
'title': qsTr("Select the game VFS path..."),
'accepted': function(path) {
gameItem.vfsPath = (''+path).substring(7);
},
},
'changed': function(newtext) { gameItem.vfsPath = newtext; },
'getValue': function() { return gameItem.vfsPath; }
},
{
'label': qsTr('VFS Sandbox:'),
'tooltip': qsTr('Where to place new (or modified) files written to the VFS.\n\nEg: /DATA/modding/%1/created').arg(gamename),
'dialogue': {
'type': 'directory',
'title': qsTr("Select the game VFS sandbox path..."),
'accepted': function(path) {
gameItem.vfsCreatedPath = (''+path).substring(7);
},
},
'changed': function(newtext) { gameItem.vfsCreatedPath = newtext; },
'getValue': function() { return gameItem.vfsCreatedPath; }
},
{
'label': qsTr('User Data Directory:'),
'tooltip': qsTr('This is the root directory of wherever your data and preferences are stored.\n\nEg: /DATA/SteamLibrary/steamapps/compatdata/%1/pfx/drive_c/users/steamuser').arg(steamid),
'dialogue': {
'type': 'directory',
'title': qsTr("Select the user data path..."),
'accepted': function(path) {
gameItem.userpath = (''+path).substring(7);
},
},
'changed': function(newtext) { gameItem.userpath = newtext; },
'getValue': function() { return gameItem.userpath; }
},
{
'label': qsTr('Quickmod Database File:'),
'tooltip': qsTr('Path to Quickmod.sqlite database.\n\nEg: /DATA/modding/%1/Quickmod.sqlite').arg(gamename),
'dialogue': {
'type': 'file',
'title': qsTr("Select the user data path..."),
'filters': ["Sqlite3 Database File (*.sqlite)"],
'fileMode': Platform.FileDialog.SaveFile,
'accepted': function(path) {
gameItem.dbpath = (''+path).substring(7);
},
},
'changed': function(newtext) { gameItem.dbpath = newtext; },
'getValue': function() { return gameItem.dbpath; }
}
]
GridLayout {
flow: GridLayout.TopToBottom
rows: pageColumn.confEntries.length
rowSpacing: 5
columnSpacing: 10
Layout.fillWidth: true
Repeater {
model: pageColumn.confEntries
delegate: Label {
text: modelData.label
enabled: cbEnabled.checked && !modelData.disabled
}
}
Repeater {
model: pageColumn.confEntries
delegate: TextField {
enabled: cbEnabled.checked && !modelData.disabled
onTextEdited: { modelData.changed(text); }
text: modelData.getValue()
Layout.fillWidth: true
ToolTip.visible: hovered && !modelData.disabled
ToolTip.text: modelData.tooltip
}
}
Repeater {
model: pageColumn.confEntries
delegate: Button {
id: disButton
text: qsTr('Browse...')
enabled: cbEnabled.checked && !modelData.disabled
onClicked: {
if( 'file' === modelData.dialogue.type ) {
fileDialogue.currentFile = 'file://' + modelData.getValue();
fileDialogue.open();
} else {
folderDialogue.currentFolder = 'file://' + modelData.getValue();
folderDialogue.open();
}
}
ToolTip.visible: hovered && !modelData.disabled
ToolTip.text: modelData.tooltip
Platform.FolderDialog {
id: folderDialogue
title: modelData.dialogue.title
onAccepted: { modelData.dialogue.accepted(folder); }
}
Platform.FileDialog {
id: fileDialogue
nameFilters: 'file' === modelData.dialogue.type ? modelData.dialogue.filters : []
fileMode: 'file' === modelData.dialogue.type ? modelData.dialogue.fileMode : Platform.FileDialog.SaveFile
title: modelData.dialogue.title
onAccepted: { modelData.dialogue.accepted(currentFile); }
}
}
}
}
ToolTip.visible: hovered
ToolTip.text: qsTr('This is where mods are extracted to and linked to the game from.\n\nEg: /DATA/modding/%1/staging').arg(gamename)
}
Button {
text: qsTr('Browse...')
height: gameItem.rowHeight
enabled: cbEnabled.checked
onClicked: {
gamePathDialogue.currentFolder = 'file://' + gamePath.text;
gamePathDialogue.open();
}
ToolTip.visible: hovered
ToolTip.text: qsTr('Where the game is installed.\n\nEg: /DATA/SteamLibrary/steamapps/common/%1-REAL').arg(gamename)
}
Button {
text: qsTr('Browse...')
height: gameItem.rowHeight
enabled: cbEnabled.checked
onClicked: {
vfsPathDialogue.currentFolder = 'file://' + vfsPathEntry.text;
vfsPathDialogue.open();
}
ToolTip.visible: hovered
ToolTip.text: qsTr('Where to mount the VFS. Typically, this will be the original GOG or Steam path.\n\nEg: /DATA/SteamLibrary/steamapps/common/%1').arg(steamid)
}
Button {
text: qsTr('Browse...')
height: gameItem.rowHeight
enabled: cbEnabled.checked
onClicked: {
vfsCreatedPathDialogue.currentFolder = 'file://' + vfsCreatedPathEntry.text;
vfsCreatedPathDialogue.open();
}
ToolTip.visible: hovered
ToolTip.text: qsTr('Where to place new (or modified) files written to the VFS.\n\nEg: /DATA/modding/%1/created').arg(gamename)
}
Button {
height: gameItem.rowHeight
text: qsTr('Browse...')
enabled: cbEnabled.checked
onClicked: {
userDataPathDialogue.currentFolder = 'file://' + userDataPath.text;
userDataPathDialogue.open();
}
ToolTip.visible: hovered
ToolTip.text: qsTr('This is the root directory of wherever your data and preferences are stored.\n\nEg: /DATA/SteamLibrary/steamapps/compatdata/%1/pfx/drive_c/users/steamuser').arg(steamid)
}
Button {
height: gameItem.rowHeight
text: qsTr('Browse...')
enabled: cbEnabled.checked
onClicked: {
dbPathDialogue.currentFile = 'file://' + userDataPath.text;
dbPathDialogue.open();
}
ToolTip.visible: hovered
ToolTip.text: qsTr('Path to Quickmod.sqlite database.\n\nEg: /DATA/modding/%1/Quickmod.sqlite').arg(gamename)
}
} // Column
Platform.FolderDialog {
id: modsPathDialogue
//visible: false
title: qsTr("Select where to store installed mods...")
onAccepted: {
modsPath.text = (''+folder).substring(7);
}
}
Platform.FolderDialog {
id: modStagingPathDialogue
//visible: false
title: qsTr("Select where to extract mods to...")
onAccepted: {
modStagingPath.text = (''+folder).substring(7);
}
}
Platform.FolderDialog {
id: gamePathDialogue
//visible: false
title: qsTr("Select the installed game path...")
onAccepted: {
gamePath.text = (''+folder).substring(7);
}
}
Platform.FolderDialog {
id: vfsPathDialogue
//visible: false
title: qsTr("Select the game VFS path...")
onAccepted: {
vfsPathEntry.text = (''+folder).substring(7);
}
}
Platform.FolderDialog {
id: vfsCreatedPathDialogue
//visible: false
title: qsTr("Select the VFS sandbox path...")
onAccepted: {
vfsCreatedPathEntry.text = (''+folder).substring(7);
}
}
Platform.FolderDialog {
id: userDataPathDialogue
//visible: false
title: qsTr("Select the user data path...")
onAccepted: {
userDataPath.text = (''+folder).substring(7);
}
}
Platform.FileDialog {
id: dbPathDialogue
//visible: false
title: qsTr("Select the Quickmod.sqlite database file...")
nameFilters: ["Sqlite3 Database File (*.sqlite)"]
onAccepted: {
dbPath.text = (''+currentFile).substring(7);
}
}
} // ColumnLayout
} // Flickable
} // Item
} // Repeater
}

View file

@ -35,8 +35,17 @@ int main(int argc, char *argv[])
app.setApplicationName("quickmod");
app.setApplicationVersion("1.0");
QStringList args;
if( !QDBusConnection::sessionBus().isConnected() )
{
qCritical() << "Cannot connect to the D-Bus session bus.\n"
<< "To start it, run:\n"
<< "\teval `dbus-launch --auto-syntax`\n";
return 1;
}
QStringList args;
QString downloadUrl;
for( int i=1; i < argc; i++ )
{
qDebug() << "arg:" << argv[i];
@ -55,14 +64,6 @@ int main(int argc, char *argv[])
return 3;
}
if( !QDBusConnection::sessionBus().isConnected() )
{
qDebug() << "Cannot connect to the D-Bus session bus.\n"
<< "To start it, run:\n"
<< "\teval `dbus-launch --auto-syntax`\n";
return 1;
}
QDBusInterface iface(SERVICE_NAME, "/", "", QDBusConnection::sessionBus());
if( iface.isValid() )
{
@ -73,11 +74,12 @@ int main(int argc, char *argv[])
}
qDebug() << "Call failed:" << qPrintable(reply.error().message());
return 1;
//return 1;
}
qDebug() << qPrintable(QDBusConnection::sessionBus().lastError().message());
return 2;
//return 2;
downloadUrl = path;
}
}