// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2026 A-La-Karte Contributors import QtQuick import QtQuick.Controls as QQC2 import QtQuick.Layouts import org.kde.kirigami as Kirigami import org.kde.kirigamiaddons.formcard as FormCard import org.kde.alakarte import "components" ColumnLayout { id: settingsPage spacing: Kirigami.Units.mediumSpacing property string runnerInstallUrl: "" property string runnerInstallSha256: "" property string runnerInstallName: "" property string compatibilityGameId: "" property string compatibilityLoadedRunnerId: "" property string compatibilityLoadedRunner: "" property string compatibilityLastError: "" property int compatibilitySpecRevision: 0 readonly property bool compatibilityHasGame: (compatibilityGameId || "").trim() !== "" property int compatibilityRunnerIndex: 0 property string compatibilityRunnerPath: "" property string compatibilityPrefixPath: "" ListModel { id: compatibilityGameChoicesModel } ListModel { id: compatibilityRunnerChoicesModel } ListModel { id: compatibilityEnvModel } ListModel { id: compatibilityExtraArgsModel } ListModel { id: compatibilityDllOverridesModel } property string lastControllerInput: "" property string lastControllerInputTime: "" property string profileMenuControllerId: "" property string newProfileName: "" property string pendingDeleteProfileId: "" property string pendingDeleteProfileName: "" function focusFirstControl() { showPlatformBadgesDelegate.forceActiveFocus() } property var pendingDisableImportApply: null property string pendingDisableImportName: "" readonly property bool anyConfirmOpen: disableImportConfirmDialog.opened || fetchAllCoversConfirmDialog.opened || removeMissingConfirmDialog.opened || clearConfirmDialog.opened || resetConfirmDialog.opened || deleteProfileConfirmDialog.opened readonly property bool anyMenuOpen: uiModeMenu.visible || profileMenu.visible function currentConfirmDialog() { if (disableImportConfirmDialog.opened) return disableImportConfirmDialog if (fetchAllCoversConfirmDialog.opened) return fetchAllCoversConfirmDialog if (removeMissingConfirmDialog.opened) return removeMissingConfirmDialog if (clearConfirmDialog.opened) return clearConfirmDialog if (resetConfirmDialog.opened) return resetConfirmDialog if (deleteProfileConfirmDialog.opened) return deleteProfileConfirmDialog return null } function closeCurrentConfirmDialog() { let d = currentConfirmDialog() if (!d) return if (typeof d.reject === "function") { d.reject() } else { d.close() } } function closeCurrentMenu() { let m = currentMenu() if (m && m.close) m.close() } function currentMenu() { if (profileMenu.visible) return profileMenu if (uiModeMenu.visible) return uiModeMenu return null } function isDescendant(item, ancestor) { let p = item while (p) { if (p === ancestor) return true if (ancestor.contentItem && p === ancestor.contentItem) return true if (p.visualParent !== undefined && p.visualParent !== null) { if (settingsPage.isDescendant(p.visualParent, ancestor)) return true } else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) { if (settingsPage.isDescendant(p.popup.visualParent, ancestor)) return true } p = p.parent } return false } function focusNextInMenu(forward) { let w = applicationWindow() if (!w || !w.activeFocusItem) return let menu = currentMenu() if (!menu) return let next = w.activeFocusItem for (let i = 0; i < 50; i++) { next = next.nextItemInFocusChain(forward) if (!next) return if (settingsPage.isDescendant(next, menu)) { next.forceActiveFocus() return } } } function rebuildCompatibilityGameChoices() { compatibilityGameChoicesModel.clear() if (!App || !App.gameModel) return for (let i = 0; i < App.gameModel.count; i++) { let g = App.gameModel.gameAt(i) if (!g) continue compatibilityGameChoicesModel.append({ gameId: String(g.id || ""), label: String(g.name || g.id || "") }) } if (compatibilityGameId === "") { if (compatibilityGameChoicesModel.count > 0) { compatibilityGameId = String(compatibilityGameChoicesModel.get(0).gameId || "") } } } function rebuildCompatibilityRunnerChoices() { compatibilityRunnerChoicesModel.clear() compatibilityRunnerChoicesModel.append({ kind: "native", label: i18n("Native"), runner: "", runnerId: "", runnerType: "" }) compatibilityRunnerChoicesModel.append({ kind: "wine", label: i18n("Wine (system)"), runner: "wine", runnerId: "", runnerType: "wine" }) compatibilityRunnerChoicesModel.append({ kind: "proton", label: i18n("Proton (auto)"), runner: "proton", runnerId: "", runnerType: "proton" }) let runners = (App.runnerManager && App.runnerManager.runners) ? App.runnerManager.runners : [] for (let i = 0; i < runners.length; i++) { let r = runners[i] if (!r) continue let name = r.name ? String(r.name) : "" let type = r.type ? String(r.type) : "" let source = r.source ? String(r.source) : "" let label = name if (type !== "" || source !== "") { label = name + " (" + type + (source !== "" ? (" · " + source) : "") + ")" } compatibilityRunnerChoicesModel.append({ kind: "inventory", label: label, runner: "", runnerId: String(r.id || ""), runnerType: type }) } compatibilityRunnerChoicesModel.append({ kind: "custom", label: i18n("Custom runner path"), runner: "custom", runnerId: "", runnerType: "custom" }) } function compatibilityIndexForGameId(gameId) { let id = (gameId || "").trim() if (id === "") return 0 for (let i = 0; i < compatibilityGameChoicesModel.count; i++) { let row = compatibilityGameChoicesModel.get(i) if (row && String(row.gameId || "") === id) return i } return 0 } function compatibilityCurrentRunnerChoice() { if (!compatibilityRunnerChoicesModel || compatibilityRunnerChoicesModel.count <= 0) return null let idx = settingsPage.compatibilityRunnerIndex if (idx < 0 || idx >= compatibilityRunnerChoicesModel.count) idx = 0 return compatibilityRunnerChoicesModel.get(idx) } function compatibilityCurrentRunnerChoiceKind() { let c = settingsPage.compatibilityCurrentRunnerChoice() if (!c || !c.kind) return "native" return String(c.kind) } function compatibilityCurrentRunnerChoiceRunnerType() { let c = settingsPage.compatibilityCurrentRunnerChoice() if (!c) return "" if (c.runnerType) return String(c.runnerType) if (c.runner) return String(c.runner) return "" } function compatibilityIndexForRunnerId(runnerId) { let id = (runnerId || "").trim() if (id === "") return 0 for (let i = 0; i < compatibilityRunnerChoicesModel.count; i++) { let row = compatibilityRunnerChoicesModel.get(i) if (row.kind === "inventory" && String(row.runnerId || "") === id) return i } return 0 } function compatibilityIndexForRunner(runner) { let r = (runner || "").trim().toLowerCase() if (r === "wine") return 1 if (r === "proton") return 2 if (r === "custom") return Math.max(0, compatibilityRunnerChoicesModel.count - 1) return 0 } function compatibilitySyncRunnerIndexFromLoaded() { if ((settingsPage.compatibilityLoadedRunnerId || "") !== "") { settingsPage.compatibilityRunnerIndex = settingsPage.compatibilityIndexForRunnerId(settingsPage.compatibilityLoadedRunnerId) return } if ((settingsPage.compatibilityLoadedRunner || "") !== "") { settingsPage.compatibilityRunnerIndex = settingsPage.compatibilityIndexForRunner(settingsPage.compatibilityLoadedRunner) return } settingsPage.compatibilityRunnerIndex = 0 } function compatibilitySetEnvFromMap(envMap) { compatibilityEnvModel.clear() if (!envMap) return let keys = Object.keys(envMap) keys.sort() for (let i = 0; i < keys.length; i++) { let k = keys[i] compatibilityEnvModel.append({ key: String(k), value: String(envMap[k] || "") }) } } function compatibilityEnvModelToMap() { let result = ({}) for (let i = 0; i < compatibilityEnvModel.count; i++) { let row = compatibilityEnvModel.get(i) let k = (row.key || "").trim() if (k === "") continue if (k.indexOf("=") !== -1) continue result[k] = String(row.value || "") } return result } function compatibilitySetExtraArgsFromList(list) { compatibilityExtraArgsModel.clear() if (!list || !Array.isArray(list)) return for (let i = 0; i < list.length; i++) { let a = String(list[i] || "").trim() if (a === "") continue compatibilityExtraArgsModel.append({ arg: a }) } } function compatibilityExtraArgsModelToList() { let result = [] for (let i = 0; i < compatibilityExtraArgsModel.count; i++) { let row = compatibilityExtraArgsModel.get(i) let a = (row.arg || "").trim() if (a === "") continue result.push(a) } return result } function compatibilitySetDllOverridesFromMap(dllMap) { compatibilityDllOverridesModel.clear() if (!dllMap) return let keys = Object.keys(dllMap) keys.sort() for (let i = 0; i < keys.length; i++) { let k = String(keys[i] || "").trim() if (k === "") continue compatibilityDllOverridesModel.append({ key: k, value: String(dllMap[k] || "") }) } } function compatibilityDllOverridesModelToMap() { let result = ({}) for (let i = 0; i < compatibilityDllOverridesModel.count; i++) { let row = compatibilityDllOverridesModel.get(i) let k = (row.key || "").trim() if (k === "") continue if (k.indexOf("=") !== -1) continue if (k.indexOf(";") !== -1) continue let v = (row.value || "").trim() if (v === "") continue result[k] = String(v) } return result } function compatibilitySetEnvVar(key, value) { let k = (key || "").trim() if (k === "") return for (let i = 0; i < compatibilityEnvModel.count; i++) { let row = compatibilityEnvModel.get(i) if ((row.key || "").trim() === k) { compatibilityEnvModel.setProperty(i, "value", String(value || "")) settingsPage.compatibilityBumpSpecRevision() return } } compatibilityEnvModel.append({ key: k, value: String(value || "") }) settingsPage.compatibilityBumpSpecRevision() } function compatibilityBumpSpecRevision() { settingsPage.compatibilitySpecRevision = settingsPage.compatibilitySpecRevision + 1 } function compatibilityResetEditor() { settingsPage.compatibilityLastError = "" settingsPage.compatibilityLoadedRunnerId = "" settingsPage.compatibilityLoadedRunner = "" settingsPage.compatibilityRunnerPath = "" settingsPage.compatibilityPrefixPath = "" settingsPage.compatibilityRunnerIndex = 0 compatibilityEnvModel.clear() compatibilityExtraArgsModel.clear() compatibilityDllOverridesModel.clear() settingsPage.compatibilityBumpSpecRevision() } function compatibilityApplyProfile(profile) { let p = profile || ({}) settingsPage.compatibilityLastError = "" settingsPage.compatibilityLoadedRunnerId = String(p.runnerId || "") settingsPage.compatibilityLoadedRunner = String(p.runner || "") settingsPage.compatibilityRunnerPath = String(p.runnerPath || "") settingsPage.compatibilityPrefixPath = String(p.prefixPath || "") settingsPage.compatibilitySyncRunnerIndexFromLoaded() settingsPage.compatibilitySetEnvFromMap(p.envOverrides || ({})) settingsPage.compatibilitySetExtraArgsFromList(p.extraArgs || ([])) settingsPage.compatibilitySetDllOverridesFromMap(p.dllOverrides || ({})) settingsPage.compatibilityBumpSpecRevision() } function compatibilityRequestProfile() { let id = (settingsPage.compatibilityGameId || "").trim() if (id === "") return if (!App || !App.runnerManager) return App.runnerManager.requestGameProfile(id) } function compatibilityBuildProfileSpec() { let id = (settingsPage.compatibilityGameId || "").trim() if (id === "") return ({}) let choice = settingsPage.compatibilityCurrentRunnerChoice() let kind = choice && choice.kind ? String(choice.kind) : "native" let runnerType = settingsPage.compatibilityCurrentRunnerChoiceRunnerType() let runnerId = choice && choice.runnerId ? String(choice.runnerId) : "" let runner = choice && choice.runner ? String(choice.runner) : "" let spec = ({ gameId: id }) if (runnerId !== "") spec.runnerId = runnerId if (runner !== "") spec.runner = runner if ((kind === "custom" || kind === "wine" || kind === "proton") && (settingsPage.compatibilityRunnerPath || "").trim() !== "") { spec.runnerPath = settingsPage.compatibilityRunnerPath.trim() } if ((runnerType === "wine" || runnerType === "proton") && (settingsPage.compatibilityPrefixPath || "").trim() !== "") { spec.prefixPath = settingsPage.compatibilityPrefixPath.trim() } let env = settingsPage.compatibilityEnvModelToMap() if (Object.keys(env).length > 0) spec.envOverrides = env let extraArgs = settingsPage.compatibilityExtraArgsModelToList() if (extraArgs.length > 0) spec.extraArgs = extraArgs let dllOverrides = settingsPage.compatibilityDllOverridesModelToMap() if (Object.keys(dllOverrides).length > 0) spec.dllOverrides = dllOverrides return spec } function compatibilityCanSaveProfile() { let spec = settingsPage.compatibilityBuildProfileSpec() if (!spec || Object.keys(spec).length <= 1) return false return true } function compatibilitySaveProfile() { let id = (settingsPage.compatibilityGameId || "").trim() if (id === "") return if (!App || !App.runnerManager) return let spec = settingsPage.compatibilityBuildProfileSpec() if (!spec || Object.keys(spec).length <= 1) return App.runnerManager.setGameProfile(spec) } function compatibilityClearProfile() { let id = (settingsPage.compatibilityGameId || "").trim() if (id === "") return if (!App || !App.runnerManager) return App.runnerManager.clearGameProfile(id) } onCompatibilityGameIdChanged: { settingsPage.compatibilityResetEditor() settingsPage.compatibilityRequestProfile() } Component.onCompleted: { settingsPage.rebuildCompatibilityGameChoices() settingsPage.rebuildCompatibilityRunnerChoices() settingsPage.compatibilityRequestProfile() } Connections { target: App && App.gameModel ? App.gameModel : null function onCountChanged() { settingsPage.rebuildCompatibilityGameChoices() } } Connections { target: App && App.runnerManager ? App.runnerManager : null function onRunnersChanged() { settingsPage.rebuildCompatibilityRunnerChoices() settingsPage.compatibilitySyncRunnerIndexFromLoaded() } function onPrefixEnsured(gameId, prefixPath) { let id = String(gameId || "") if (id !== settingsPage.compatibilityGameId) return settingsPage.compatibilityLastError = "" if ((settingsPage.compatibilityPrefixPath || "").trim() !== "") { settingsPage.compatibilityPrefixPath = String(prefixPath || "") } } function onPrefixDeleted(gameId, prefixPath) { let id = String(gameId || "") if (id !== settingsPage.compatibilityGameId) return settingsPage.compatibilityLastError = "" } function onGameProfileFetched(gameId, result) { let id = String(gameId || "") if (id !== settingsPage.compatibilityGameId) return if (!result || result.ok !== true) { settingsPage.compatibilityLastError = result && result.error ? String(result.error) : i18n("Failed to load profile") settingsPage.compatibilityApplyProfile({}) return } settingsPage.compatibilityApplyProfile(result.profile || ({})) } function onGameProfileSaved(gameId, result) { let id = String(gameId || "") if (id !== settingsPage.compatibilityGameId) return if (!result || result.ok !== true) { settingsPage.compatibilityLastError = result && result.error ? String(result.error) : i18n("Failed to save profile") return } settingsPage.compatibilityApplyProfile(result.profile || ({})) } function onGameProfileCleared(gameId, result) { let id = String(gameId || "") if (id !== settingsPage.compatibilityGameId) return if (!result || result.ok !== true) { settingsPage.compatibilityLastError = result && result.error ? String(result.error) : i18n("Failed to clear profile") return } settingsPage.compatibilityResetEditor() } } FormCard.FormHeader { Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("Controller") } FormCard.FormCard { Layout.fillWidth: true FormCard.FormTextDelegate { text: GamepadManager.connected ? i18n("Connected") : i18n("Not connected") description: GamepadManager.connected ? (GamepadManager.name !== "" ? GamepadManager.name : i18n("Gamepad")) : i18n("Connect a controller to use couch navigation") textItem.wrapMode: Text.WordWrap } FormCard.FormDelegateSeparator {} FormCard.FormTextDelegate { text: i18n("Device") description: { if (!GamepadManager.connected) return i18n("-") let parts = [] if (GamepadManager.vendorId > 0) parts.push(i18n("Vendor: %1").arg(GamepadManager.vendorId)) if (GamepadManager.productId > 0) parts.push(i18n("Product: %1").arg(GamepadManager.productId)) return parts.length > 0 ? parts.join(" · ") : i18n("-") } textItem.wrapMode: Text.WordWrap descriptionItem.wrapMode: Text.WordWrap } FormCard.FormDelegateSeparator {} FormCard.FormTextDelegate { text: i18n("Buttons") description: { if (!GamepadManager.connected) return i18n("-") return i18n("Confirm: %1 · Back: %2 · Details: %3 · Search: %4") .arg(GamepadManager.confirmButtonLabel) .arg(GamepadManager.backButtonLabel) .arg(GamepadManager.detailsButtonLabel) .arg(GamepadManager.searchButtonLabel) } textItem.wrapMode: Text.WordWrap descriptionItem.wrapMode: Text.WordWrap } FormCard.FormDelegateSeparator {} FormCard.FormTextDelegate { text: i18n("Active input") description: { if (InputManager.activeInput === InputManager.Gamepad) return i18n("Gamepad") return i18n("Keyboard & Mouse") } textItem.wrapMode: Text.WordWrap } FormCard.FormDelegateSeparator {} FormCard.FormTextDelegate { text: i18n("Input service") description: { if (App.inputService.available) return i18n("Available") if (App.inputService.lastError !== "") return App.inputService.lastError return i18n("Unavailable") } descriptionItem.wrapMode: Text.WordWrap } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Refresh controllers") icon.name: "view-refresh" onClicked: App.inputService.refreshControllers() } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Refresh profiles") icon.name: "view-refresh" onClicked: App.inputService.refreshProfiles() } } FormCard.FormHeader { Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("Controllers") } FormCard.FormCard { Layout.fillWidth: true Repeater { model: App.inputService.controllers ColumnLayout { Layout.fillWidth: true readonly property var controller: modelData readonly property string controllerId: controller && controller.controllerId ? controller.controllerId : "" readonly property string controllerName: controller && controller.name ? controller.name : controllerId readonly property string connectionState: controller && controller.connectionState ? controller.connectionState : "" readonly property int batteryPercent: controller && controller.batteryPercent !== undefined ? controller.batteryPercent : -1 readonly property string batteryState: controller && controller.batteryStateName ? controller.batteryStateName : "" readonly property string activeProfileName: controller && controller.activeProfileName ? controller.activeProfileName : "" readonly property string activeProfileId: controller && controller.activeProfileId ? controller.activeProfileId : "default" FormCard.FormTextDelegate { text: controllerName description: { let parts = [] if (connectionState !== "") parts.push(connectionState) if (batteryPercent >= 0) { parts.push(i18n("Battery: %1%2").arg(batteryPercent).arg("%")) } else if (batteryState !== "") { parts.push(i18n("Battery: %1").arg(batteryState)) } return parts.length > 0 ? parts.join(" · ") : "" } textItem.wrapMode: Text.WordWrap descriptionItem.wrapMode: Text.WordWrap } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Profile") description: activeProfileName !== "" ? activeProfileName : i18n("Default") icon.name: "preferences-desktop-gaming" enabled: controllerId !== "" && App.inputService.profiles.length > 0 onClicked: { settingsPage.profileMenuControllerId = controllerId profileMenu.open() } } FormCard.FormDelegateSeparator {} } } FormCard.FormTextDelegate { visible: App.inputService.controllers.length === 0 text: i18n("No controllers detected") textItem.font: Kirigami.Theme.smallFont } } QQC2.Menu { id: profileMenu focus: true onOpened: Qt.callLater(function() { if (profileMenu.contentChildren.length > 0 && profileMenu.contentChildren[0].forceActiveFocus) { profileMenu.contentChildren[0].forceActiveFocus() } }) Repeater { model: App.inputService.profiles delegate: QQC2.MenuItem { readonly property var profile: modelData readonly property string profileId: profile && profile.id ? profile.id : "" text: profile && profile.name ? profile.name : profileId checkable: true checked: profileId !== "" && profileId === settingsPage.currentProfileIdForMenu() onTriggered: { if (profileId === "" || settingsPage.profileMenuControllerId === "") return App.inputService.setActiveProfile(settingsPage.profileMenuControllerId, profileId) } } } } function currentProfileIdForMenu() { for (let i = 0; i < App.inputService.controllers.length; i++) { let c = App.inputService.controllers[i] if (c && c.controllerId === settingsPage.profileMenuControllerId) { return c.activeProfileId ? c.activeProfileId : "default" } } return "default" } FormCard.FormHeader { Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("Controller Profiles") } FormCard.FormCard { Layout.fillWidth: true FormCard.FormTextFieldDelegate { label: i18n("New profile name") text: settingsPage.newProfileName placeholderText: i18n("e.g. Couch") onTextChanged: settingsPage.newProfileName = text } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Create profile") icon.name: "list-add" enabled: settingsPage.newProfileName.trim() !== "" && App.inputService.available onClicked: { App.inputService.createProfile(settingsPage.newProfileName.trim()) settingsPage.newProfileName = "" } } FormCard.FormDelegateSeparator {} Repeater { model: App.inputService.profiles ColumnLayout { Layout.fillWidth: true readonly property var profile: modelData readonly property string profileId: profile && profile.id ? profile.id : "" readonly property string profileName: profile && profile.name ? profile.name : profileId FormCard.FormTextDelegate { text: profileName description: profileId descriptionItem.wrapMode: Text.WrapAnywhere descriptionItem.font: Kirigami.Theme.smallFont } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Delete") icon.name: "edit-delete" enabled: profileId !== "" && profileId !== "default" && App.inputService.available onClicked: { settingsPage.pendingDeleteProfileId = profileId settingsPage.pendingDeleteProfileName = profileName deleteProfileConfirmDialog.open() } } FormCard.FormDelegateSeparator {} } } } FormCard.FormHeader { Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("Input Test") } FormCard.FormCard { Layout.fillWidth: true FormCard.FormTextDelegate { text: settingsPage.lastControllerInput !== "" ? settingsPage.lastControllerInput : i18n("No input yet") description: settingsPage.lastControllerInputTime textItem.wrapMode: Text.WordWrap descriptionItem.wrapMode: Text.WordWrap } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Clear") icon.name: "edit-clear" enabled: settingsPage.lastControllerInput !== "" onClicked: { settingsPage.lastControllerInput = "" settingsPage.lastControllerInputTime = "" } } } function activateFocusedInMenu() { let w = applicationWindow() if (!w || !w.activeFocusItem) return let menu = currentMenu() if (!menu) return let item = w.activeFocusItem if (typeof item.triggered === "function") { item.triggered() return } if (typeof item.clicked === "function") { item.clicked() return } } function focusNoButton(dialog) { Qt.callLater(function() { if (!dialog) return if (typeof dialog.standardButton === "function") { let noButton = dialog.standardButton(Kirigami.Dialog.No) if (noButton) { noButton.forceActiveFocus() return } } dialog.forceActiveFocus() }) } function requestDisableImport(sourceName, applyFn) { pendingDisableImportName = sourceName pendingDisableImportApply = applyFn disableImportConfirmDialog.open() } FormCard.FormHeader { Layout.fillWidth: true title: i18n("Appearance") } FormCard.FormCard { Layout.fillWidth: true FormCard.FormCheckDelegate { id: showPlatformBadgesDelegate text: i18n("Show platform badges") description: i18n("Display platform icons on game cards") checked: App.config.showPlatformBadges onToggled: App.config.showPlatformBadges = checked } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { id: uiModeDelegate text: i18n("UI mode") description: { if (App.config.uiMode === Config.Desktop) return i18n("Desktop") if (App.config.uiMode === Config.Couch) return i18n("Couch") return i18n("Automatic") } icon.name: "view-fullscreen" onClicked: uiModeMenu.open() QQC2.Menu { id: uiModeMenu focus: true onOpened: Qt.callLater(function() { uiModeAuto.forceActiveFocus() }) QQC2.MenuItem { id: uiModeAuto text: i18n("Automatic") checkable: true checked: App.config.uiMode === Config.Auto onTriggered: App.config.uiMode = Config.Auto } QQC2.MenuItem { id: uiModeDesktop text: i18n("Desktop") checkable: true checked: App.config.uiMode === Config.Desktop onTriggered: App.config.uiMode = Config.Desktop } QQC2.MenuItem { id: uiModeCouch text: i18n("Couch") checkable: true checked: App.config.uiMode === Config.Couch onTriggered: App.config.uiMode = Config.Couch } } } } Connections { target: GamepadManager function onNavigateUp() { if (!settingsPage.anyMenuOpen) return let menu = settingsPage.currentMenu() if (!menu) return let w = applicationWindow() if (!w || !w.activeFocusItem) return if (!settingsPage.isDescendant(w.activeFocusItem, menu)) return settingsPage.focusNextInMenu(false) } function onNavigateDown() { if (!settingsPage.anyMenuOpen) return let menu = settingsPage.currentMenu() if (!menu) return let w = applicationWindow() if (!w || !w.activeFocusItem) return if (!settingsPage.isDescendant(w.activeFocusItem, menu)) return settingsPage.focusNextInMenu(true) } function onNavigateLeft() { if (!settingsPage.anyMenuOpen) return let menu = settingsPage.currentMenu() if (!menu) return let w = applicationWindow() if (!w || !w.activeFocusItem) return if (!settingsPage.isDescendant(w.activeFocusItem, menu)) return settingsPage.focusNextInMenu(false) } function onNavigateRight() { if (!settingsPage.anyMenuOpen) return let menu = settingsPage.currentMenu() if (!menu) return let w = applicationWindow() if (!w || !w.activeFocusItem) return if (!settingsPage.isDescendant(w.activeFocusItem, menu)) return settingsPage.focusNextInMenu(true) } function onSelectPressed() { if (!settingsPage.anyMenuOpen) return let menu = settingsPage.currentMenu() if (!menu) return let w = applicationWindow() if (!w || !w.activeFocusItem) return if (!settingsPage.isDescendant(w.activeFocusItem, menu)) return settingsPage.activateFocusedInMenu() } function onBackPressed() { settingsPage.lastControllerInput = i18n("Back") settingsPage.lastControllerInputTime = new Date().toLocaleTimeString() } function onDetailsPressed() { settingsPage.lastControllerInput = i18n("Details") settingsPage.lastControllerInputTime = new Date().toLocaleTimeString() } function onMenuPressed() { settingsPage.lastControllerInput = i18n("Menu") settingsPage.lastControllerInputTime = new Date().toLocaleTimeString() } function onSearchPressed() { settingsPage.lastControllerInput = i18n("Search") settingsPage.lastControllerInputTime = new Date().toLocaleTimeString() } function onLeftBumperPressed() { settingsPage.lastControllerInput = i18n("Left bumper") settingsPage.lastControllerInputTime = new Date().toLocaleTimeString() } function onRightBumperPressed() { settingsPage.lastControllerInput = i18n("Right bumper") settingsPage.lastControllerInputTime = new Date().toLocaleTimeString() } } Kirigami.PromptDialog { id: deleteProfileConfirmDialog title: i18n("Delete Profile") subtitle: settingsPage.pendingDeleteProfileName !== "" ? i18n("Delete '%1'?", settingsPage.pendingDeleteProfileName) : i18n("Delete this profile?") standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No onOpened: settingsPage.focusNoButton(deleteProfileConfirmDialog) onAccepted: { if (settingsPage.pendingDeleteProfileId !== "") { App.inputService.deleteProfile(settingsPage.pendingDeleteProfileId) } settingsPage.pendingDeleteProfileId = "" settingsPage.pendingDeleteProfileName = "" } onRejected: { settingsPage.pendingDeleteProfileId = "" settingsPage.pendingDeleteProfileName = "" } } FormCard.FormHeader { Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("Import Sources") } FormCard.FormCard { Layout.fillWidth: true FormCard.FormCheckDelegate { text: i18n("Steam") icon.name: "" leading: IconWithResourceFallback { primary: "com.valvesoftware.Steam" secondary: "steam" resourceFallback: Qt.resolvedUrl("icons/brand/steam-symbolic.svg") } property bool restoring: false checked: App.config.importSteam onToggled: { if (restoring) return if (checked === App.config.importSteam) return if (!checked && App.gameModel.hasPlatformPrefix("Steam")) { restoring = true checked = Qt.binding(function() { return App.config.importSteam }) restoring = false settingsPage.requestDisableImport(i18n("Steam"), function() { App.config.importSteam = false }) return } App.config.importSteam = checked restoring = true checked = Qt.binding(function() { return App.config.importSteam }) restoring = false } } FormCard.FormDelegateSeparator {} FormCard.FormCheckDelegate { text: i18n("Lutris") icon.name: "" leading: IconWithResourceFallback { primary: "lutris" secondary: "applications-games" } property bool restoring: false checked: App.config.importLutris onToggled: { if (restoring) return if (checked === App.config.importLutris) return if (!checked && App.gameModel.hasPlatformPrefix("Lutris")) { restoring = true checked = Qt.binding(function() { return App.config.importLutris }) restoring = false settingsPage.requestDisableImport(i18n("Lutris"), function() { App.config.importLutris = false }) return } App.config.importLutris = checked restoring = true checked = Qt.binding(function() { return App.config.importLutris }) restoring = false } } FormCard.FormDelegateSeparator {} FormCard.FormCheckDelegate { text: i18n("Heroic Games Launcher") description: i18n("Epic, GOG, and Amazon games") icon.name: "" leading: IconWithResourceFallback { primary: "com.heroicgameslauncher.hgl" secondary: "applications-games" } property bool restoring: false checked: App.config.importHeroic onToggled: { if (restoring) return if (checked === App.config.importHeroic) return if (!checked && App.gameModel.hasPlatformPrefix("Heroic")) { restoring = true checked = Qt.binding(function() { return App.config.importHeroic }) restoring = false settingsPage.requestDisableImport(i18n("Heroic Games Launcher"), function() { App.config.importHeroic = false }) return } App.config.importHeroic = checked restoring = true checked = Qt.binding(function() { return App.config.importHeroic }) restoring = false } } FormCard.FormDelegateSeparator {} FormCard.FormCheckDelegate { text: i18n("Desktop Entries") description: i18n("Games from .desktop files") icon.name: "" leading: IconWithResourceFallback { primary: "user-desktop" secondary: "computer" } property bool restoring: false checked: App.config.importDesktop onToggled: { if (restoring) return if (checked === App.config.importDesktop) return if (!checked && App.gameModel.hasPlatformPrefix("Desktop")) { restoring = true checked = Qt.binding(function() { return App.config.importDesktop }) restoring = false settingsPage.requestDisableImport(i18n("Desktop Entries"), function() { App.config.importDesktop = false }) return } App.config.importDesktop = checked restoring = true checked = Qt.binding(function() { return App.config.importDesktop }) restoring = false } } FormCard.FormDelegateSeparator {} FormCard.FormCheckDelegate { text: i18n("Bottles") description: i18n("Wine applications") icon.name: "" leading: IconWithResourceFallback { primary: "com.usebottles.bottles" secondary: "application-x-executable" } property bool restoring: false checked: App.config.importBottles onToggled: { if (restoring) return if (checked === App.config.importBottles) return if (!checked && App.gameModel.hasPlatformPrefix("Bottles")) { restoring = true checked = Qt.binding(function() { return App.config.importBottles }) restoring = false settingsPage.requestDisableImport(i18n("Bottles"), function() { App.config.importBottles = false }) return } App.config.importBottles = checked restoring = true checked = Qt.binding(function() { return App.config.importBottles }) restoring = false } } FormCard.FormDelegateSeparator {} FormCard.FormCheckDelegate { text: i18n("Flatpak") description: i18n("Flatpak game applications") icon.name: "" leading: IconWithResourceFallback { primary: "flatpak-discover" secondary: "applications-games" } property bool restoring: false checked: App.config.importFlatpak onToggled: { if (restoring) return if (checked === App.config.importFlatpak) return if (!checked && App.gameModel.hasPlatformPrefix("Flatpak")) { restoring = true checked = Qt.binding(function() { return App.config.importFlatpak }) restoring = false settingsPage.requestDisableImport(i18n("Flatpak"), function() { App.config.importFlatpak = false }) return } App.config.importFlatpak = checked restoring = true checked = Qt.binding(function() { return App.config.importFlatpak }) restoring = false } } FormCard.FormDelegateSeparator {} FormCard.FormCheckDelegate { text: i18n("itch.io") description: i18n("Games from itch.io app") icon.name: "" leading: IconWithResourceFallback { primary: "io.itch.itch" secondary: "itch" resourceFallback: Qt.resolvedUrl("icons/brand/itchdotio-symbolic.svg") } property bool restoring: false checked: App.config.importItch onToggled: { if (restoring) return if (checked === App.config.importItch) return if (!checked && App.gameModel.hasPlatformPrefix("itch.io")) { restoring = true checked = Qt.binding(function() { return App.config.importItch }) restoring = false settingsPage.requestDisableImport(i18n("itch.io"), function() { App.config.importItch = false }) return } App.config.importItch = checked restoring = true checked = Qt.binding(function() { return App.config.importItch }) restoring = false } } FormCard.FormDelegateSeparator {} FormCard.FormCheckDelegate { text: i18n("Legendary") description: i18n("Epic Games via Legendary CLI") icon.name: "" leading: IconWithResourceFallback { primary: "legendary" secondary: "applications-games" } property bool restoring: false checked: App.config.importLegendary onToggled: { if (restoring) return if (checked === App.config.importLegendary) return if (!checked && App.gameModel.hasPlatformPrefix("Legendary")) { restoring = true checked = Qt.binding(function() { return App.config.importLegendary }) restoring = false settingsPage.requestDisableImport(i18n("Legendary"), function() { App.config.importLegendary = false }) return } App.config.importLegendary = checked restoring = true checked = Qt.binding(function() { return App.config.importLegendary }) restoring = false } } FormCard.FormDelegateSeparator {} FormCard.FormCheckDelegate { text: i18n("RetroArch") description: i18n("Games from RetroArch playlists") icon.name: "" leading: IconWithResourceFallback { primary: "org.libretro.RetroArch" secondary: "retroarch" resourceFallback: Qt.resolvedUrl("icons/brand/retroarch-symbolic.svg") } property bool restoring: false checked: App.config.importRetroArch onToggled: { if (restoring) return if (checked === App.config.importRetroArch) return if (!checked && App.gameModel.hasPlatformPrefix("RetroArch")) { restoring = true checked = Qt.binding(function() { return App.config.importRetroArch }) restoring = false settingsPage.requestDisableImport(i18n("RetroArch"), function() { App.config.importRetroArch = false }) return } App.config.importRetroArch = checked restoring = true checked = Qt.binding(function() { return App.config.importRetroArch }) restoring = false } } FormCard.FormDelegateSeparator {} FormCard.FormCheckDelegate { text: i18n("Auto-import on startup") description: i18n("Scan for new games when launching") checked: App.config.autoImportOnStartup onToggled: App.config.autoImportOnStartup = checked } } FormCard.FormHeader { Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("Behavior") } FormCard.FormCard { Layout.fillWidth: true FormCard.FormCheckDelegate { text: i18n("Exit after launching game") description: i18n("Close A-La-Karte when a game starts") checked: App.config.exitAfterLaunch onToggled: App.config.exitAfterLaunch = checked } FormCard.FormDelegateSeparator {} FormCard.FormCheckDelegate { text: i18n("Click cover to launch") description: i18n("Clicking the cover launches the game instead of showing details") checked: App.config.coverLaunchesGame onToggled: App.config.coverLaunchesGame = checked } } FormCard.FormHeader { Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("Runners") } FormCard.FormCard { Layout.fillWidth: true FormCard.FormTextFieldDelegate { label: i18n("Archive URL or local path") text: settingsPage.runnerInstallUrl placeholderText: i18n("https://... or /home/.../file.tar.gz") enabled: !App.runnerManager.busy onTextChanged: settingsPage.runnerInstallUrl = text } FormCard.FormDelegateSeparator {} FormCard.FormTextFieldDelegate { label: i18n("SHA256/SHA512 (optional)") text: settingsPage.runnerInstallSha256 placeholderText: i18n("Leave empty to skip verification") enabled: !App.runnerManager.busy onTextChanged: settingsPage.runnerInstallSha256 = text } FormCard.FormDelegateSeparator {} FormCard.FormTextFieldDelegate { label: i18n("Name (optional)") text: settingsPage.runnerInstallName placeholderText: i18n("Proton-GE") enabled: !App.runnerManager.busy onTextChanged: settingsPage.runnerInstallName = text } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Install from URL") description: App.runnerManager.busy ? App.runnerManager.status : i18n("Download and install runner") icon.name: "download" enabled: settingsPage.runnerInstallUrl.trim() !== "" && !App.runnerManager.busy onClicked: App.runnerManager.installRunnerFromUrl(settingsPage.runnerInstallUrl, settingsPage.runnerInstallSha256, settingsPage.runnerInstallName) } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Cancel install") icon.name: "dialog-cancel" enabled: App.runnerManager.busy onClicked: App.runnerManager.cancelCurrentInstall() } FormCard.FormDelegateSeparator {} Item { Layout.fillWidth: true Layout.margins: Kirigami.Units.largeSpacing ColumnLayout { anchors.left: parent.left anchors.right: parent.right spacing: Kirigami.Units.smallSpacing QQC2.Label { Layout.fillWidth: true text: App.runnerManager.status wrapMode: Text.WordWrap visible: App.runnerManager.status !== "" } QQC2.Label { Layout.fillWidth: true text: App.runnerManager.lastError wrapMode: Text.WordWrap visible: App.runnerManager.lastError !== "" color: Kirigami.Theme.negativeTextColor } QQC2.ProgressBar { Layout.fillWidth: true from: 0 to: App.runnerManager.totalBytes > 0 ? App.runnerManager.totalBytes : 1 value: App.runnerManager.receivedBytes indeterminate: App.runnerManager.busy && App.runnerManager.totalBytes <= 0 visible: App.runnerManager.busy } } } } FormCard.FormHeader { Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("Installed Runners") } FormCard.FormCard { Layout.fillWidth: true FormCard.FormButtonDelegate { text: i18n("Refresh") icon.name: "view-refresh" enabled: !App.runnerManager.busy onClicked: App.runnerManager.refreshRunners() } FormCard.FormDelegateSeparator {} Repeater { model: App.runnerManager.runners ColumnLayout { Layout.fillWidth: true readonly property var runner: modelData readonly property string runnerId: (runner && runner.id) ? runner.id : "" readonly property string runnerName: (runner && runner.name) ? runner.name : runnerId readonly property string runnerType: (runner && runner.type) ? runner.type : "" readonly property string runnerSource: (runner && runner.source) ? runner.source : "" readonly property string runnerPath: (runner && runner.path) ? runner.path : "" FormCard.FormTextDelegate { text: runnerName description: (runnerType !== "" ? (runnerType + " · ") : "") + (runnerSource !== "" ? runnerSource : "") textItem.wrapMode: Text.WordWrap } FormCard.FormDelegateSeparator {} FormCard.FormTextDelegate { text: runnerPath textItem.wrapMode: Text.WrapAnywhere textItem.font: Kirigami.Theme.smallFont } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Uninstall") icon.name: "edit-delete" enabled: runnerSource === "installed" && runnerId !== "" && !App.runnerManager.busy onClicked: App.runnerManager.uninstallRunner(runnerId) } FormCard.FormDelegateSeparator {} } } FormCard.FormTextDelegate { visible: App.runnerManager.runners.length === 0 text: i18n("No runners found") textItem.font: Kirigami.Theme.smallFont } } FormCard.FormHeader { Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("Compatibility") } FormCard.FormCard { Layout.fillWidth: true FormCard.FormComboBoxDelegate { text: i18n("Game") model: compatibilityGameChoicesModel textRole: "label" currentIndex: settingsPage.compatibilityIndexForGameId(settingsPage.compatibilityGameId) onActivated: { if (currentIndex < 0 || currentIndex >= compatibilityGameChoicesModel.count) return settingsPage.compatibilityGameId = String(compatibilityGameChoicesModel.get(currentIndex).gameId || "") } } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Reload profile") icon.name: "view-refresh" enabled: settingsPage.compatibilityHasGame onClicked: settingsPage.compatibilityRequestProfile() } FormCard.FormDelegateSeparator {} FormCard.FormTextDelegate { visible: settingsPage.compatibilityLastError !== "" text: settingsPage.compatibilityLastError textItem.wrapMode: Text.WordWrap textItem.color: Kirigami.Theme.negativeTextColor textItem.font: Kirigami.Theme.smallFont } FormCard.FormDelegateSeparator {} FormCard.FormComboBoxDelegate { text: i18n("Runner") model: compatibilityRunnerChoicesModel textRole: "label" currentIndex: settingsPage.compatibilityRunnerIndex enabled: settingsPage.compatibilityHasGame onActivated: { settingsPage.compatibilityRunnerIndex = currentIndex settingsPage.compatibilityBumpSpecRevision() } } FormCard.FormDelegateSeparator {} FormCard.FormTextFieldDelegate { label: i18n("Runner Path") text: settingsPage.compatibilityRunnerPath placeholderText: settingsPage.compatibilityCurrentRunnerChoiceKind() === "proton" ? i18n("Auto-detect Proton") : (settingsPage.compatibilityCurrentRunnerChoiceKind() === "wine" ? i18n("Use system Wine") : i18n("Required for Custom")) enabled: settingsPage.compatibilityHasGame && (settingsPage.compatibilityCurrentRunnerChoiceKind() === "custom" || settingsPage.compatibilityCurrentRunnerChoiceKind() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceKind() === "proton") onTextChanged: { settingsPage.compatibilityRunnerPath = text settingsPage.compatibilityBumpSpecRevision() } } FormCard.FormDelegateSeparator {} FormCard.FormTextFieldDelegate { label: i18n("Prefix Path") text: settingsPage.compatibilityPrefixPath placeholderText: i18n("Default (AppDataLocation/prefixes/)") enabled: settingsPage.compatibilityHasGame && (settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton") onTextChanged: { settingsPage.compatibilityPrefixPath = text settingsPage.compatibilityBumpSpecRevision() } } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Create Prefix") icon.name: "folder-new" enabled: settingsPage.compatibilityHasGame && (settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton") onClicked: App.runnerManager.ensurePrefix(settingsPage.compatibilityGameId, settingsPage.compatibilityCurrentRunnerChoiceRunnerType(), settingsPage.compatibilityPrefixPath.trim()) } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Delete Prefix") icon.name: "edit-delete" enabled: settingsPage.compatibilityHasGame && (settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton") && (settingsPage.compatibilityPrefixPath.trim() === "") onClicked: App.runnerManager.deletePrefix(settingsPage.compatibilityGameId, settingsPage.compatibilityPrefixPath.trim()) } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Reset Prefix to Default") icon.name: "edit-clear" enabled: settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton" onClicked: settingsPage.compatibilityPrefixPath = "" } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Save Profile") icon.name: "document-save" enabled: settingsPage.compatibilityHasGame && settingsPage.compatibilitySpecRevision >= 0 && settingsPage.compatibilityCanSaveProfile() onClicked: settingsPage.compatibilitySaveProfile() } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Clear Profile") icon.name: "edit-delete" enabled: settingsPage.compatibilityHasGame onClicked: settingsPage.compatibilityClearProfile() } } FormCard.FormCard { Layout.fillWidth: true Layout.topMargin: Kirigami.Units.largeSpacing FormCard.FormHeader { title: i18n("Environment Variables") } FormCard.FormTextDelegate { text: i18n("These variables are applied only when launching this game from A-La-Karte.") textItem.wrapMode: Text.WordWrap textItem.font: Kirigami.Theme.smallFont } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Add Variable") icon.name: "list-add" enabled: settingsPage.compatibilityHasGame onClicked: { compatibilityEnvModel.append({ key: "", value: "" }) settingsPage.compatibilityBumpSpecRevision() } } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Preset: MangoHud") description: i18n("Sets MANGOHUD=1") icon.name: "games-hint" enabled: settingsPage.compatibilityHasGame onClicked: settingsPage.compatibilitySetEnvVar("MANGOHUD", "1") } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Preset: GameMode") description: i18n("Sets GAMEMODERUN=1") icon.name: "speedometer" enabled: settingsPage.compatibilityHasGame onClicked: settingsPage.compatibilitySetEnvVar("GAMEMODERUN", "1") } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Clear Variables") icon.name: "edit-clear" enabled: settingsPage.compatibilityHasGame && compatibilityEnvModel.count > 0 onClicked: { compatibilityEnvModel.clear() settingsPage.compatibilityBumpSpecRevision() } } ColumnLayout { Layout.fillWidth: true Layout.margins: Kirigami.Units.largeSpacing spacing: Kirigami.Units.smallSpacing Repeater { model: compatibilityEnvModel RowLayout { Layout.fillWidth: true spacing: Kirigami.Units.smallSpacing QQC2.TextField { Layout.fillWidth: true Layout.preferredWidth: 1 placeholderText: i18n("KEY") text: key enabled: settingsPage.compatibilityHasGame onTextChanged: { compatibilityEnvModel.setProperty(index, "key", text) settingsPage.compatibilityBumpSpecRevision() } } QQC2.TextField { Layout.fillWidth: true Layout.preferredWidth: 2 placeholderText: i18n("VALUE") text: value enabled: settingsPage.compatibilityHasGame onTextChanged: { compatibilityEnvModel.setProperty(index, "value", text) settingsPage.compatibilityBumpSpecRevision() } } QQC2.ToolButton { icon.name: "list-remove" enabled: settingsPage.compatibilityHasGame onClicked: { compatibilityEnvModel.remove(index) settingsPage.compatibilityBumpSpecRevision() } } } } } } FormCard.FormCard { Layout.fillWidth: true Layout.topMargin: Kirigami.Units.largeSpacing FormCard.FormHeader { title: i18n("Extra Launch Arguments") } FormCard.FormTextDelegate { text: i18n("These arguments are appended when launching this game from A-La-Karte.") textItem.wrapMode: Text.WordWrap textItem.font: Kirigami.Theme.smallFont } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Add Argument") icon.name: "list-add" enabled: settingsPage.compatibilityHasGame onClicked: { compatibilityExtraArgsModel.append({ arg: "" }) settingsPage.compatibilityBumpSpecRevision() } } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Clear Arguments") icon.name: "edit-clear" enabled: settingsPage.compatibilityHasGame && compatibilityExtraArgsModel.count > 0 onClicked: { compatibilityExtraArgsModel.clear() settingsPage.compatibilityBumpSpecRevision() } } ColumnLayout { Layout.fillWidth: true Layout.margins: Kirigami.Units.largeSpacing spacing: Kirigami.Units.smallSpacing Repeater { model: compatibilityExtraArgsModel RowLayout { Layout.fillWidth: true spacing: Kirigami.Units.smallSpacing QQC2.TextField { Layout.fillWidth: true placeholderText: i18n("ARG") text: arg enabled: settingsPage.compatibilityHasGame onTextChanged: { compatibilityExtraArgsModel.setProperty(index, "arg", text) settingsPage.compatibilityBumpSpecRevision() } } QQC2.ToolButton { icon.name: "list-remove" enabled: settingsPage.compatibilityHasGame onClicked: { compatibilityExtraArgsModel.remove(index) settingsPage.compatibilityBumpSpecRevision() } } } } } } FormCard.FormCard { Layout.fillWidth: true Layout.topMargin: Kirigami.Units.largeSpacing FormCard.FormHeader { title: i18n("Wine DLL Overrides") } FormCard.FormTextDelegate { text: i18n("These overrides are applied to Wine/Proton launches via WINEDLLOVERRIDES.") textItem.wrapMode: Text.WordWrap textItem.font: Kirigami.Theme.smallFont } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Add Override") icon.name: "list-add" enabled: settingsPage.compatibilityHasGame && (settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton") onClicked: { compatibilityDllOverridesModel.append({ key: "", value: "" }) settingsPage.compatibilityBumpSpecRevision() } } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Clear Overrides") icon.name: "edit-clear" enabled: settingsPage.compatibilityHasGame && compatibilityDllOverridesModel.count > 0 onClicked: { compatibilityDllOverridesModel.clear() settingsPage.compatibilityBumpSpecRevision() } } ColumnLayout { Layout.fillWidth: true Layout.margins: Kirigami.Units.largeSpacing spacing: Kirigami.Units.smallSpacing Repeater { model: compatibilityDllOverridesModel RowLayout { Layout.fillWidth: true spacing: Kirigami.Units.smallSpacing QQC2.TextField { Layout.fillWidth: true Layout.preferredWidth: 1 placeholderText: i18n("DLL") text: key enabled: settingsPage.compatibilityHasGame && (settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton") onTextChanged: { compatibilityDllOverridesModel.setProperty(index, "key", text) settingsPage.compatibilityBumpSpecRevision() } } QQC2.TextField { Layout.fillWidth: true Layout.preferredWidth: 2 placeholderText: i18n("OVERRIDE") text: value enabled: settingsPage.compatibilityHasGame && (settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton") onTextChanged: { compatibilityDllOverridesModel.setProperty(index, "value", text) settingsPage.compatibilityBumpSpecRevision() } } QQC2.ToolButton { icon.name: "list-remove" enabled: settingsPage.compatibilityHasGame && (settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton") onClicked: { compatibilityDllOverridesModel.remove(index) settingsPage.compatibilityBumpSpecRevision() } } } } } } FormCard.FormHeader { Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("Cover Art") } FormCard.FormCard { Layout.fillWidth: true FormCard.FormCheckDelegate { text: i18n("High quality images") description: i18n("Sharper covers at the cost of memory") checked: App.config.highQualityImages onToggled: App.config.highQualityImages = checked } FormCard.FormDelegateSeparator {} FormCard.FormCheckDelegate { text: i18n("Animated covers") description: i18n("Play animated cover art when available") checked: App.config.animatedCovers onToggled: App.config.animatedCovers = checked } } FormCard.FormHeader { Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("SteamGridDB") } FormCard.FormCard { Layout.fillWidth: true FormCard.FormCheckDelegate { text: i18n("Enable SteamGridDB") description: i18n("Automatically download cover art") checked: App.steamGridDB.enabled onToggled: App.steamGridDB.enabled = checked } FormCard.FormDelegateSeparator {} FormCard.FormTextFieldDelegate { label: i18n("SteamGridDB API Key") text: App.steamGridDB.apiKey placeholderText: i18n("Enter your API key") echoMode: TextInput.Password enabled: App.steamGridDB.enabled onTextChanged: App.steamGridDB.apiKey = text } FormCard.FormDelegateSeparator {} FormCard.FormCheckDelegate { text: i18n("Prefer SteamGridDB covers") description: i18n("Replace existing covers with SteamGridDB art") checked: App.steamGridDB.preferSteamGridDB enabled: App.steamGridDB.enabled onToggled: App.steamGridDB.preferSteamGridDB = checked } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Fetch All Covers") description: App.steamGridDB.busy ? i18n("Fetching...") : i18n("Download covers for all games") icon.name: "download" enabled: App.steamGridDB.enabled && App.steamGridDB.apiKey.length > 0 && !App.steamGridDB.busy onClicked: fetchAllCoversConfirmDialog.open() } FormCard.FormDelegateSeparator {} FormCard.FormTextDelegate { text: i18n("Get a free API key at steamgriddb.com") textItem.font: Kirigami.Theme.smallFont textItem.color: Kirigami.Theme.linkColor TapHandler { onTapped: Qt.openUrlExternally("https://www.steamgriddb.com/profile/preferences/api") } } } FormCard.FormHeader { Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("Library") } FormCard.FormCard { Layout.fillWidth: true FormCard.FormButtonDelegate { text: i18n("Import All Games") description: i18n("Scan all enabled sources") icon.name: "document-import" enabled: !App.importing && App.gameModel.count >= 0 && ((App.config.importSteam && !App.gameModel.hasPlatformPrefix("Steam")) || (App.config.importLutris && !App.gameModel.hasPlatformPrefix("Lutris")) || (App.config.importHeroic && !App.gameModel.hasPlatformPrefix("Heroic")) || (App.config.importDesktop && !App.gameModel.hasPlatformPrefix("Desktop")) || (App.config.importBottles && !App.gameModel.hasPlatformPrefix("Bottles")) || (App.config.importFlatpak && !App.gameModel.hasPlatformPrefix("Flatpak")) || (App.config.importItch && !App.gameModel.hasPlatformPrefix("itch.io")) || (App.config.importLegendary && !App.gameModel.hasPlatformPrefix("Legendary")) || (App.config.importRetroArch && !App.gameModel.hasPlatformPrefix("RetroArch"))) onClicked: App.importAllGames() } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Remove Missing Games") description: i18n("Remove games whose executables no longer exist") icon.name: "edit-delete" onClicked: removeMissingConfirmDialog.open() } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Clear Library") description: i18n("Remove all games") icon.name: "edit-clear-all" onClicked: clearConfirmDialog.open() } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { text: i18n("Reset Application") description: i18n("Clear library and reset all settings to defaults") icon.name: "edit-reset" onClicked: resetConfirmDialog.open() } } Kirigami.PromptDialog { id: disableImportConfirmDialog title: i18n("Disable Import Source") subtitle: i18n("Disabling %1 will remove all games imported from that source. Are you sure?", settingsPage.pendingDisableImportName) standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No onOpened: settingsPage.focusNoButton(disableImportConfirmDialog) onAccepted: { if (settingsPage.pendingDisableImportApply) { settingsPage.pendingDisableImportApply() } settingsPage.pendingDisableImportApply = null settingsPage.pendingDisableImportName = "" } onRejected: { settingsPage.pendingDisableImportApply = null settingsPage.pendingDisableImportName = "" } } Kirigami.PromptDialog { id: fetchAllCoversConfirmDialog title: i18n("Fetch All Covers") subtitle: App.steamGridDB.preferSteamGridDB ? i18n("This will download cover art for all games and may replace existing covers. Continue?") : i18n("This will download cover art for games that are missing covers. Continue?") standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No onOpened: settingsPage.focusNoButton(fetchAllCoversConfirmDialog) onAccepted: App.steamGridDB.fetchAllCovers() } Kirigami.PromptDialog { id: removeMissingConfirmDialog title: i18n("Remove Missing Games") subtitle: i18n("This will remove games whose executables cannot be found. This cannot be undone. Continue?") standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No onOpened: settingsPage.focusNoButton(removeMissingConfirmDialog) onAccepted: App.removeMissingGames() } Kirigami.PromptDialog { id: clearConfirmDialog title: i18n("Clear Library") subtitle: i18n("Are you sure you want to remove all games?") standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No onOpened: settingsPage.focusNoButton(clearConfirmDialog) onAccepted: App.clearLibrary() } Kirigami.PromptDialog { id: resetConfirmDialog title: i18n("Reset Application") subtitle: i18n("This will remove all games and reset all settings to defaults. Are you sure?") standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No onOpened: settingsPage.focusNoButton(resetConfirmDialog) onAccepted: { App.clearLibrary() App.config.resetToDefaults() } } }