import QtQuick 2.9 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 import StreamingPreferences 1.0 import ComputerManager 1.0 import SdlGamepadKeyNavigation 1.0 import SystemProperties 1.0 Flickable { id: settingsPage objectName: qsTr("Settings") signal languageChanged() boundsBehavior: Flickable.OvershootBounds contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width contentHeight: settingsColumn1.height > settingsColumn2.height ? settingsColumn1.height : settingsColumn2.height ScrollBar.vertical: ScrollBar { anchors { left: parent.right leftMargin: -10 } } function isChildOfFlickable(item) { while (item) { if (item.parent === contentItem) { return true } item = item.parent } return false } NumberAnimation on contentY { id: autoScrollAnimation duration: 100 } Window.onActiveFocusItemChanged: { var item = Window.activeFocusItem if (item) { // Ignore non-child elements like the toolbar buttons if (!isChildOfFlickable(item)) { return } // Map the focus item's position into our content item's coordinate space var pos = item.mapToItem(contentItem, 0, 0) // Ensure some extra space is visible around the element we're scrolling to var scrollMargin = height > 100 ? 50 : 0 if (pos.y - scrollMargin < contentY) { autoScrollAnimation.from = contentY autoScrollAnimation.to = Math.max(pos.y - scrollMargin, 0) autoScrollAnimation.start() } else if (pos.y + item.height + scrollMargin > contentY + height) { autoScrollAnimation.from = contentY autoScrollAnimation.to = Math.min(pos.y + item.height + scrollMargin - height, contentHeight - height) autoScrollAnimation.start() } } } StackView.onActivated: { // This enables Tab and BackTab based navigation rather than arrow keys. // It is required to shift focus between controls on the settings page. SdlGamepadKeyNavigation.setUiNavMode(true) // Highlight the first item if a gamepad is connected if (SdlGamepadKeyNavigation.getConnectedGamepads() > 0) { resolutionComboBox.forceActiveFocus(Qt.TabFocus) } } StackView.onDeactivating: { SdlGamepadKeyNavigation.setUiNavMode(false) // Save the prefs so the Session can observe the changes StreamingPreferences.save() } Component.onDestruction: { // Also save preferences on destruction, since we won't get a // deactivating callback if the user just closes Moonlight StreamingPreferences.save() } Column { padding: 10 id: settingsColumn1 width: settingsPage.width / 2 spacing: 15 GroupBox { id: basicSettingsGroupBox width: (parent.width - (parent.leftPadding + parent.rightPadding)) padding: 12 title: "" + qsTr("Basic Settings") + "" font.pointSize: 12 Column { anchors.fill: parent spacing: 5 Label { width: parent.width id: resFPStitle text: qsTr("Resolution and FPS") font.pointSize: 12 wrapMode: Text.Wrap } Label { width: parent.width id: resFPSdesc text: qsTr("Setting values too high for your PC or network connection may cause lag, stuttering, or errors.") font.pointSize: 9 wrapMode: Text.Wrap } Row { spacing: 5 width: parent.width AutoResizingComboBox { property int lastIndexValue function addDetectedResolution(friendlyNamePrefix, rect) { var indexToAdd = 0 for (var j = 0; j < resolutionComboBox.count; j++) { var existing_width = parseInt(resolutionListModel.get(j).video_width); var existing_height = parseInt(resolutionListModel.get(j).video_height); if (rect.width === existing_width && rect.height === existing_height) { // Duplicate entry, skip indexToAdd = -1 break } else if (rect.width * rect.height > existing_width * existing_height) { // Candidate entrypoint after this entry indexToAdd = j + 1 } } // Insert this display's resolution if it's not a duplicate if (indexToAdd >= 0) { resolutionListModel.insert(indexToAdd, { "text": friendlyNamePrefix+" ("+rect.width+"x"+rect.height+")", "video_width": ""+rect.width, "video_height": ""+rect.height, "is_custom": false }) } } // ignore setting the index at first, and actually set it when the component is loaded Component.onCompleted: { // Refresh display data before using it to build the list SystemProperties.refreshDisplays() // Add native and safe area resolutions for all attached displays var done = false for (var displayIndex = 0; !done; displayIndex++) { var screenRect = SystemProperties.getNativeResolution(displayIndex); var safeAreaRect = SystemProperties.getSafeAreaResolution(displayIndex); if (screenRect.width === 0) { // Exceeded max count of displays done = true break } addDetectedResolution(qsTr("Native"), screenRect) addDetectedResolution(qsTr("Native (Excluding Notch)"), safeAreaRect) } // Prune resolutions that are over the decoder's maximum var max_pixels = SystemProperties.maximumResolution.width * SystemProperties.maximumResolution.height; if (max_pixels > 0) { for (var j = 0; j < resolutionComboBox.count; j++) { var existing_width = parseInt(resolutionListModel.get(j).video_width); var existing_height = parseInt(resolutionListModel.get(j).video_height); if (existing_width * existing_height > max_pixels) { resolutionListModel.remove(j) j-- } } } // load the saved width/height, and iterate through the ComboBox until a match is found // and set it to that index. var saved_width = StreamingPreferences.width var saved_height = StreamingPreferences.height var index_set = false for (var i = 0; i < resolutionListModel.count; i++) { var el_width = parseInt(resolutionListModel.get(i).video_width); var el_height = parseInt(resolutionListModel.get(i).video_height); if (saved_width === el_width && saved_height === el_height) { currentIndex = i index_set = true break } } if (!index_set) { // We did not find a match. This must be a custom resolution. resolutionListModel.append({ "text": qsTr("Custom")+" ("+StreamingPreferences.width+"x"+StreamingPreferences.height+")", "video_width": ""+StreamingPreferences.width, "video_height": ""+StreamingPreferences.height, "is_custom": true }) currentIndex = resolutionListModel.count - 1 } else { resolutionListModel.append({ "text": qsTr("Custom"), "video_width": "", "video_height": "", "is_custom": true }) } // Since we don't call activate() here, we need to trigger // width calculation manually recalculateWidth() lastIndexValue = currentIndex } id: resolutionComboBox maximumWidth: parent.width / 2 textRole: "text" model: ListModel { id: resolutionListModel // Other elements may be added at runtime // based on attached display resolution ListElement { text: qsTr("720p") video_width: "1280" video_height: "720" is_custom: false } ListElement { text: qsTr("1080p") video_width: "1920" video_height: "1080" is_custom: false } ListElement { text: qsTr("1440p") video_width: "2560" video_height: "1440" is_custom: false } ListElement { text: qsTr("4K") video_width: "3840" video_height: "2160" is_custom: false } } function updateBitrateForSelection() { var selectedWidth = parseInt(resolutionListModel.get(currentIndex).video_width) var selectedHeight = parseInt(resolutionListModel.get(currentIndex).video_height) // Only modify the bitrate if the values actually changed if (StreamingPreferences.width !== selectedWidth || StreamingPreferences.height !== selectedHeight) { StreamingPreferences.width = selectedWidth StreamingPreferences.height = selectedHeight StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.height, StreamingPreferences.fps, StreamingPreferences.enableYUV444); slider.value = StreamingPreferences.bitrateKbps } lastIndexValue = currentIndex } // ::onActivated must be used, as it only listens for when the index is changed by a human onActivated : { if (resolutionListModel.get(currentIndex).is_custom) { customResolutionDialog.open() } else { updateBitrateForSelection() } } NavigableDialog { id: customResolutionDialog standardButtons: Dialog.Ok | Dialog.Cancel onOpened: { // Force keyboard focus on the textbox so keyboard navigation works widthField.forceActiveFocus() // standardButton() was added in Qt 5.10, so we must check for it first if (customResolutionDialog.standardButton) { customResolutionDialog.standardButton(Dialog.Ok).enabled = customResolutionDialog.isInputValid() } } onClosed: { widthField.clear() heightField.clear() } onRejected: { resolutionComboBox.currentIndex = resolutionComboBox.lastIndexValue } function isInputValid() { // If we have text in either textbox that isn't valid, // reject the input. if ((!widthField.acceptableInput && widthField.text) || (!heightField.acceptableInput && heightField.text)) { return false } // The textboxes need to have text or placeholder text if ((!widthField.text && !widthField.placeholderText) || (!heightField.text && !heightField.placeholderText)) { return false } return true } onAccepted: { // Reject if there's invalid input if (!isInputValid()) { reject() return } var width = widthField.text ? widthField.text : widthField.placeholderText var height = heightField.text ? heightField.text : heightField.placeholderText // Find and update the custom entry for (var i = 0; i < resolutionListModel.count; i++) { if (resolutionListModel.get(i).is_custom) { resolutionListModel.setProperty(i, "video_width", width) resolutionListModel.setProperty(i, "video_height", height) resolutionListModel.setProperty(i, "text", "Custom ("+width+"x"+height+")") // Now update the bitrate using the custom resolution resolutionComboBox.currentIndex = i resolutionComboBox.updateBitrateForSelection() // Update the combobox width too resolutionComboBox.recalculateWidth() break } } } ColumnLayout { Label { text: qsTr("Custom resolutions are not officially supported by GeForce Experience, so it will not set your host display resolution. You will need to set it manually while in game.") + "\n\n" + qsTr("Resolutions that are not supported by your client or host PC may cause streaming errors.") + "\n" wrapMode: Label.WordWrap Layout.maximumWidth: 300 } Label { text: qsTr("Enter a custom resolution:") font.bold: true } RowLayout { TextField { id: widthField maximumLength: 5 inputMethodHints: Qt.ImhDigitsOnly placeholderText: resolutionListModel.get(resolutionComboBox.currentIndex).video_width validator: IntValidator{bottom:256; top:8192} focus: true onTextChanged: { // standardButton() was added in Qt 5.10, so we must check for it first if (customResolutionDialog.standardButton) { customResolutionDialog.standardButton(Dialog.Ok).enabled = customResolutionDialog.isInputValid() } } Keys.onReturnPressed: { customResolutionDialog.accept() } Keys.onEnterPressed: { customResolutionDialog.accept() } } Label { text: "x" font.bold: true } TextField { id: heightField maximumLength: 5 inputMethodHints: Qt.ImhDigitsOnly placeholderText: resolutionListModel.get(resolutionComboBox.currentIndex).video_height validator: IntValidator{bottom:256; top:8192} onTextChanged: { // standardButton() was added in Qt 5.10, so we must check for it first if (customResolutionDialog.standardButton) { customResolutionDialog.standardButton(Dialog.Ok).enabled = customResolutionDialog.isInputValid() } } Keys.onReturnPressed: { customResolutionDialog.accept() } Keys.onEnterPressed: { customResolutionDialog.accept() } } } } } } AutoResizingComboBox { property int lastIndexValue function updateBitrateForSelection() { // Only modify the bitrate if the values actually changed var selectedFps = parseInt(model.get(fpsComboBox.currentIndex).video_fps) if (StreamingPreferences.fps !== selectedFps) { StreamingPreferences.fps = selectedFps StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.height, StreamingPreferences.fps, StreamingPreferences.enableYUV444); slider.value = StreamingPreferences.bitrateKbps } lastIndexValue = currentIndex } NavigableDialog { function isInputValid() { // If we have text that isn't valid, reject the input. if (!fpsField.acceptableInput && fpsField.text) { return false } // The textbox needs to have text or placeholder text if (!fpsField.text && !fpsField.placeholderText) { return false } return true } id: customFpsDialog standardButtons: Dialog.Ok | Dialog.Cancel onOpened: { // Force keyboard focus on the textbox so keyboard navigation works fpsField.forceActiveFocus() // standardButton() was added in Qt 5.10, so we must check for it first if (customFpsDialog.standardButton) { customFpsDialog.standardButton(Dialog.Ok).enabled = customFpsDialog.isInputValid() } } onClosed: { fpsField.clear() } onRejected: { fpsComboBox.currentIndex = fpsComboBox.lastIndexValue } onAccepted: { // Reject if there's invalid input if (!isInputValid()) { reject() return } var fps = fpsField.text ? fpsField.text : fpsField.placeholderText // Find and update the custom entry for (var i = 0; i < fpsListModel.count; i++) { if (fpsListModel.get(i).is_custom) { fpsListModel.setProperty(i, "video_fps", fps) fpsListModel.setProperty(i, "text", qsTr("Custom (%1 FPS)").arg(fps)) // Now update the bitrate using the custom resolution fpsComboBox.currentIndex = i fpsComboBox.updateBitrateForSelection() // Update the combobox width too fpsComboBox.recalculateWidth() break } } } ColumnLayout { Label { text: qsTr("Enter a custom frame rate:") font.bold: true } RowLayout { TextField { id: fpsField maximumLength: 4 inputMethodHints: Qt.ImhDigitsOnly placeholderText: fpsListModel.get(fpsComboBox.currentIndex).video_fps validator: IntValidator{bottom:10; top:9999} focus: true onTextChanged: { // standardButton() was added in Qt 5.10, so we must check for it first if (customFpsDialog.standardButton) { customFpsDialog.standardButton(Dialog.Ok).enabled = customFpsDialog.isInputValid() } } Keys.onReturnPressed: { customFpsDialog.accept() } Keys.onEnterPressed: { customFpsDialog.accept() } } } } } function addRefreshRateOrdered(fpsListModel, refreshRate, description, custom) { var indexToAdd = 0 for (var j = 0; j < fpsListModel.count; j++) { var existing_fps = parseInt(fpsListModel.get(j).video_fps); if (refreshRate === existing_fps || (custom && fpsListModel.get(j).is_custom)) { // Duplicate entry, skip indexToAdd = -1 break } else if (refreshRate > existing_fps) { // Candidate entrypoint after this entry indexToAdd = j + 1 } } // Insert this frame rate if it's not a duplicate if (indexToAdd >= 0) { // Custom values always go at the end of the list if (custom) { indexToAdd = fpsListModel.count } fpsListModel.insert(indexToAdd, { "text": description, "video_fps": ""+refreshRate, "is_custom": custom }) } return indexToAdd } function reinitialize() { // Add native refresh rate for all attached displays var done = false for (var displayIndex = 0; !done; displayIndex++) { var refreshRate = SystemProperties.getRefreshRate(displayIndex); if (refreshRate === 0) { // Exceeded max count of displays done = true break } addRefreshRateOrdered(fpsListModel, refreshRate, qsTr("%1 FPS").arg(refreshRate), false) } var saved_fps = StreamingPreferences.fps var found = false for (var i = 0; i < model.count; i++) { var el_fps = parseInt(model.get(i).video_fps); // Look for a matching frame rate if (saved_fps === el_fps) { currentIndex = i found = true break } } // If we didn't find one, add a custom frame rate for the current value if (!found) { currentIndex = addRefreshRateOrdered(model, saved_fps, qsTr("Custom (%1 FPS)").arg(saved_fps), true) } else { addRefreshRateOrdered(model, "", qsTr("Custom"), true) } recalculateWidth() lastIndexValue = currentIndex } // ignore setting the index at first, and actually set it when the component is loaded Component.onCompleted: { reinitialize() languageChanged.connect(reinitialize) } model: ListModel { id: fpsListModel // Other elements may be added at runtime ListElement { text: qsTr("30 FPS") video_fps: "30" is_custom: false } ListElement { text: qsTr("60 FPS") video_fps: "60" is_custom: false } } id: fpsComboBox maximumWidth: parent.width / 2 textRole: "text" // ::onActivated must be used, as it only listens for when the index is changed by a human onActivated : { if (model.get(currentIndex).is_custom) { customFpsDialog.open() } else { updateBitrateForSelection() } } } } Label { width: parent.width id: bitrateTitle text: qsTr("Video bitrate:") font.pointSize: 12 wrapMode: Text.Wrap } Label { width: parent.width id: bitrateDesc text: qsTr("Lower the bitrate on slower connections. Raise the bitrate to increase image quality.") font.pointSize: 9 wrapMode: Text.Wrap } Slider { id: slider value: StreamingPreferences.bitrateKbps stepSize: 500 from : 500 to: StreamingPreferences.unlockBitrate ? 500000 : 150000 snapMode: "SnapOnRelease" width: Math.min(bitrateDesc.implicitWidth, parent.width) onValueChanged: { bitrateTitle.text = qsTr("Video bitrate: %1 Mbps").arg(value / 1000.0) StreamingPreferences.bitrateKbps = value } Component.onCompleted: { // Refresh the text after translations change languageChanged.connect(valueChanged) } } Label { width: parent.width id: windowModeTitle text: qsTr("Display mode") font.pointSize: 12 wrapMode: Text.Wrap visible: SystemProperties.hasDesktopEnvironment } AutoResizingComboBox { function createModel() { var model = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '') model.append({ text: qsTr("Fullscreen"), val: StreamingPreferences.WM_FULLSCREEN }) model.append({ text: qsTr("Borderless windowed"), val: StreamingPreferences.WM_FULLSCREEN_DESKTOP }) model.append({ text: qsTr("Windowed"), val: StreamingPreferences.WM_WINDOWED }) // Set the recommended option based on the OS for (var i = 0; i < model.count; i++) { var thisWm = model.get(i).val; if (thisWm === StreamingPreferences.recommendedFullScreenMode) { model.get(i).text += " " + qsTr("(Recommended)") model.move(i, 0, 1) break } } return model } // This is used on initialization and upon retranslation function reinitialize() { if (!visible) { // Do nothing if the control won't even be visible return } model = createModel() currentIndex = 0 // Set the current value based on the saved preferences var savedWm = StreamingPreferences.windowMode for (var i = 0; i < model.count; i++) { var thisWm = model.get(i).val; if (savedWm === thisWm) { currentIndex = i break } } activated(currentIndex) } Component.onCompleted: { reinitialize() languageChanged.connect(reinitialize) } id: windowModeComboBox visible: SystemProperties.hasDesktopEnvironment enabled: !SystemProperties.rendererAlwaysFullScreen hoverEnabled: true textRole: "text" onActivated: { StreamingPreferences.windowMode = model.get(currentIndex).val } ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: qsTr("Fullscreen generally provides the best performance, but borderless windowed may work better with features like macOS Spaces, Alt+Tab, screenshot tools, on-screen overlays, etc.") } CheckBox { id: vsyncCheck width: parent.width hoverEnabled: true text: qsTr("V-Sync") font.pointSize: 12 checked: StreamingPreferences.enableVsync onCheckedChanged: { StreamingPreferences.enableVsync = checked } ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: qsTr("Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing") } CheckBox { id: framePacingCheck width: parent.width hoverEnabled: true text: qsTr("Frame pacing") font.pointSize: 12 enabled: StreamingPreferences.enableVsync checked: StreamingPreferences.enableVsync && StreamingPreferences.framePacing onCheckedChanged: { StreamingPreferences.framePacing = checked } ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: qsTr("Frame pacing reduces micro-stutter by delaying frames that come in too early") } } } GroupBox { id: audioSettingsGroupBox width: (parent.width - (parent.leftPadding + parent.rightPadding)) padding: 12 title: "" + qsTr("Audio Settings") + "" font.pointSize: 12 Column { anchors.fill: parent spacing: 5 Label { width: parent.width id: resAudioTitle text: qsTr("Audio configuration") font.pointSize: 12 wrapMode: Text.Wrap } AutoResizingComboBox { // ignore setting the index at first, and actually set it when the component is loaded Component.onCompleted: { var saved_audio = StreamingPreferences.audioConfig currentIndex = 0 for (var i = 0; i < audioListModel.count; i++) { var el_audio = audioListModel.get(i).val; if (saved_audio === el_audio) { currentIndex = i break } } activated(currentIndex) } id: audioComboBox textRole: "text" model: ListModel { id: audioListModel ListElement { text: qsTr("Stereo") val: StreamingPreferences.AC_STEREO } ListElement { text: qsTr("5.1 surround sound") val: StreamingPreferences.AC_51_SURROUND } ListElement { text: qsTr("7.1 surround sound") val: StreamingPreferences.AC_71_SURROUND } } // ::onActivated must be used, as it only listens for when the index is changed by a human onActivated : { StreamingPreferences.audioConfig = audioListModel.get(currentIndex).val } } CheckBox { id: audioPcCheck width: parent.width text: qsTr("Mute host PC speakers while streaming") font.pointSize: 12 checked: !StreamingPreferences.playAudioOnHost onCheckedChanged: { StreamingPreferences.playAudioOnHost = !checked } ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: qsTr("You must restart any game currently in progress for this setting to take effect") } CheckBox { id: muteOnFocusLossCheck width: parent.width text: qsTr("Mute audio stream when Moonlight is not the active window") font.pointSize: 12 visible: SystemProperties.hasDesktopEnvironment checked: StreamingPreferences.muteOnFocusLoss onCheckedChanged: { StreamingPreferences.muteOnFocusLoss = checked } ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: qsTr("Mutes Moonlight's audio when you Alt+Tab out of the stream or click on a different window.") } } } GroupBox { id: hostSettingsGroupBox width: (parent.width - (parent.leftPadding + parent.rightPadding)) padding: 12 title: "" + qsTr("Host Settings") + "" font.pointSize: 12 Column { anchors.fill: parent spacing: 5 CheckBox { id: optimizeGameSettingsCheck width: parent.width text: qsTr("Optimize game settings for streaming") font.pointSize: 12 checked: StreamingPreferences.gameOptimizations onCheckedChanged: { StreamingPreferences.gameOptimizations = checked } } CheckBox { id: quitAppAfter width: parent.width text: qsTr("Quit app on host PC after ending stream") font.pointSize: 12 checked: StreamingPreferences.quitAppAfter onCheckedChanged: { StreamingPreferences.quitAppAfter = checked } ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: qsTr("This will close the app or game you are streaming when you end your stream. You will lose any unsaved progress!") } } } GroupBox { id: uiSettingsGroupBox width: (parent.width - (parent.leftPadding + parent.rightPadding)) padding: 12 title: "" + qsTr("UI Settings") + "" font.pointSize: 12 Column { anchors.fill: parent spacing: 5 Label { width: parent.width id: languageTitle text: qsTr("Language") font.pointSize: 12 wrapMode: Text.Wrap } AutoResizingComboBox { // ignore setting the index at first, and actually set it when the component is loaded Component.onCompleted: { var saved_language = StreamingPreferences.language currentIndex = 0 for (var i = 0; i < languageListModel.count; i++) { var el_language = languageListModel.get(i).val; if (saved_language === el_language) { currentIndex = i break } } activated(currentIndex) } id: languageComboBox textRole: "text" model: ListModel { id: languageListModel ListElement { text: qsTr("Automatic") val: StreamingPreferences.LANG_AUTO } ListElement { text: "Deutsch" // German val: StreamingPreferences.LANG_DE } ListElement { text: "English" val: StreamingPreferences.LANG_EN } ListElement { text: "Français" // French val: StreamingPreferences.LANG_FR } ListElement { text: "简体中文" // Simplified Chinese val: StreamingPreferences.LANG_ZH_CN } ListElement { text: "Norwegian Bokmål" val: StreamingPreferences.LANG_NB_NO } ListElement { text: "русский" // Russian val: StreamingPreferences.LANG_RU } ListElement { text: "Español" // Spanish val: StreamingPreferences.LANG_ES } ListElement { text: "日本語" // Japanese val: StreamingPreferences.LANG_JA } ListElement { text: "Tiếng Việt" // Vietnamese val: StreamingPreferences.LANG_VI } ListElement { text: "ภาษาไทย" // Thai val: StreamingPreferences.LANG_TH } ListElement { text: "한국어" // Korean val: StreamingPreferences.LANG_KO } ListElement { text: "Magyar" // Hungarian val: StreamingPreferences.LANG_HU } ListElement { text: "Nederlands" // Dutch val: StreamingPreferences.LANG_NL } ListElement { text: "Svenska" // Swedish val: StreamingPreferences.LANG_SV } ListElement { text: "Türkçe" // Turkish val: StreamingPreferences.LANG_TR } /* ListElement { text: "Українська" // Ukrainian val: StreamingPreferences.LANG_UK } */ ListElement { text: "繁體中文" // Traditional Chinese val: StreamingPreferences.LANG_ZH_TW } ListElement { text: "Português" // Portuguese val: StreamingPreferences.LANG_PT } /* ListElement { text: "Português do Brasil" // Brazilian Portuguese val: StreamingPreferences.LANG_PT_BR } */ ListElement { text: "Ελληνικά" // Greek val: StreamingPreferences.LANG_EL } ListElement { text: "Italiano" // Italian val: StreamingPreferences.LANG_IT } /* ListElement { text: "हिन्दी, हिंदी" // Hindi val: StreamingPreferences.LANG_HI } */ ListElement { text: "Język polski" // Polish val: StreamingPreferences.LANG_PL } ListElement { text: "Čeština" // Czech val: StreamingPreferences.LANG_CS } /* ListElement { text: "עִבְרִית" // Hebrew val: StreamingPreferences.LANG_HE } */ /* ListElement { text: "کرمانجیی خواروو" // Central Kurdish val: StreamingPreferences.LANG_CKB } */ /* ListElement { text: "Lietuvių kalba" // Lithuanian val: StreamingPreferences.LANG_LT } */ /* ListElement { text: "Eesti" // Estonian val: StreamingPreferences.LANG_ET } */ } // ::onActivated must be used, as it only listens for when the index is changed by a human onActivated : { // Retranslating is expensive, so only do it if the language actually changed var new_language = languageListModel.get(currentIndex).val if (StreamingPreferences.language !== new_language) { StreamingPreferences.language = languageListModel.get(currentIndex).val if (!StreamingPreferences.retranslate()) { ToolTip.show(qsTr("You must restart Moonlight for this change to take effect"), 5000) } else { // Force the back operation to pop any AppView pages that exist. // The AppView stops working after retranslate() for some reason. window.clearOnBack = true // Signal other controls to adjust their text languageChanged() } } } } Label { width: parent.width id: uiDisplayModeTitle text: qsTr("GUI display mode") font.pointSize: 12 wrapMode: Text.Wrap visible: SystemProperties.hasDesktopEnvironment } AutoResizingComboBox { // ignore setting the index at first, and actually set it when the component is loaded Component.onCompleted: { if (!visible) { // Do nothing if the control won't even be visible return } var saved_uidisplaymode = StreamingPreferences.uiDisplayMode currentIndex = 0 for (var i = 0; i < uiDisplayModeListModel.count; i++) { var el_uidisplaymode = uiDisplayModeListModel.get(i).val; if (saved_uidisplaymode === el_uidisplaymode) { currentIndex = i break } } activated(currentIndex) } id: uiDisplayModeComboBox visible: SystemProperties.hasDesktopEnvironment textRole: "text" model: ListModel { id: uiDisplayModeListModel ListElement { text: qsTr("Windowed") val: StreamingPreferences.UI_WINDOWED } ListElement { text: qsTr("Maximized") val: StreamingPreferences.UI_MAXIMIZED } ListElement { text: qsTr("Fullscreen") val: StreamingPreferences.UI_FULLSCREEN } } // ::onActivated must be used, as it only listens for when the index is changed by a human onActivated : { StreamingPreferences.uiDisplayMode = uiDisplayModeListModel.get(currentIndex).val } } CheckBox { id: connectionWarningsCheck width: parent.width text: qsTr("Show connection quality warnings") font.pointSize: 12 checked: StreamingPreferences.connectionWarnings onCheckedChanged: { StreamingPreferences.connectionWarnings = checked } } CheckBox { visible: SystemProperties.hasDiscordIntegration id: discordPresenceCheck width: parent.width text: qsTr("Discord Rich Presence integration") font.pointSize: 12 checked: StreamingPreferences.richPresence onCheckedChanged: { StreamingPreferences.richPresence = checked } ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: qsTr("Updates your Discord status to display the name of the game you're streaming.") } CheckBox { id: keepAwakeCheck width: parent.width text: qsTr("Keep the display awake while streaming") font.pointSize: 12 checked: StreamingPreferences.keepAwake onCheckedChanged: { StreamingPreferences.keepAwake = checked } ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: qsTr("Prevents the screensaver from starting or the display from going to sleep while streaming.") } } } } Column { padding: 10 rightPadding: 20 anchors.left: settingsColumn1.right id: settingsColumn2 width: settingsPage.width / 2 spacing: 15 GroupBox { id: inputSettingsGroupBox width: (parent.width - (parent.leftPadding + parent.rightPadding)) padding: 12 title: "" + qsTr("Input Settings") + "" font.pointSize: 12 Column { anchors.fill: parent spacing: 5 CheckBox { id: absoluteMouseCheck hoverEnabled: true width: parent.width text: qsTr("Optimize mouse for remote desktop instead of games") font.pointSize: 12 checked: StreamingPreferences.absoluteMouseMode onCheckedChanged: { StreamingPreferences.absoluteMouseMode = checked } ToolTip.delay: 1000 ToolTip.timeout: 10000 ToolTip.visible: hovered ToolTip.text: qsTr("This enables seamless mouse control without capturing the client's mouse cursor. It is ideal for remote desktop usage but will not work in most games.") + " " + qsTr("You can toggle this while streaming using Ctrl+Alt+Shift+M.") + "\n\n" + qsTr("NOTE: Due to a bug in GeForce Experience, this option may not work properly if your host PC has multiple monitors.") } Row { spacing: 5 width: parent.width CheckBox { id: captureSysKeysCheck hoverEnabled: true text: qsTr("Capture system keyboard shortcuts") font.pointSize: 12 enabled: SystemProperties.hasDesktopEnvironment checked: StreamingPreferences.captureSysKeysMode !== StreamingPreferences.CSK_OFF || !SystemProperties.hasDesktopEnvironment ToolTip.delay: 1000 ToolTip.timeout: 10000 ToolTip.visible: hovered ToolTip.text: qsTr("This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming.") + "\n\n" + qsTr("NOTE: Certain keyboard shortcuts like Ctrl+Alt+Del on Windows cannot be intercepted by any application, including Moonlight.") } AutoResizingComboBox { // ignore setting the index at first, and actually set it when the component is loaded Component.onCompleted: { if (!visible) { // Do nothing if the control won't even be visible return } var saved_syskeysmode = StreamingPreferences.captureSysKeysMode currentIndex = 0 for (var i = 0; i < captureSysKeysModeListModel.count; i++) { var el_syskeysmode = captureSysKeysModeListModel.get(i).val; if (saved_syskeysmode === el_syskeysmode) { currentIndex = i break } } activated(currentIndex) } enabled: captureSysKeysCheck.checked && captureSysKeysCheck.enabled textRole: "text" model: ListModel { id: captureSysKeysModeListModel ListElement { text: qsTr("in fullscreen") val: StreamingPreferences.CSK_FULLSCREEN } ListElement { text: qsTr("always") val: StreamingPreferences.CSK_ALWAYS } } function updatePref() { if (!enabled) { StreamingPreferences.captureSysKeysMode = StreamingPreferences.CSK_OFF } else { StreamingPreferences.captureSysKeysMode = captureSysKeysModeListModel.get(currentIndex).val } } // ::onActivated must be used, as it only listens for when the index is changed by a human onActivated: { updatePref() } // This handles transition of the checkbox state onEnabledChanged: { updatePref() } } } CheckBox { id: absoluteTouchCheck hoverEnabled: true width: parent.width text: qsTr("Use touchscreen as a virtual trackpad") font.pointSize: 12 checked: !StreamingPreferences.absoluteTouchMode onCheckedChanged: { StreamingPreferences.absoluteTouchMode = !checked } ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: qsTr("When checked, the touchscreen acts like a trackpad. When unchecked, the touchscreen will directly control the mouse pointer.") } CheckBox { id: swapMouseButtonsCheck hoverEnabled: true width: parent.width text: qsTr("Swap left and right mouse buttons") font.pointSize: 12 checked: StreamingPreferences.swapMouseButtons onCheckedChanged: { StreamingPreferences.swapMouseButtons = checked } } CheckBox { id: reverseScrollButtonsCheck hoverEnabled: true width: parent.width text: qsTr("Reverse mouse scrolling direction") font.pointSize: 12 checked: StreamingPreferences.reverseScrollDirection onCheckedChanged: { StreamingPreferences.reverseScrollDirection = checked } } } } GroupBox { id: gamepadSettingsGroupBox width: (parent.width - (parent.leftPadding + parent.rightPadding)) padding: 12 title: "" + qsTr("Gamepad Settings") + "" font.pointSize: 12 Column { anchors.fill: parent spacing: 5 CheckBox { id: swapFaceButtonsCheck width: parent.width text: qsTr("Swap A/B and X/Y gamepad buttons") font.pointSize: 12 checked: StreamingPreferences.swapFaceButtons onCheckedChanged: { StreamingPreferences.swapFaceButtons = checked } ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: qsTr("This switches gamepads into a Nintendo-style button layout") } CheckBox { id: singleControllerCheck width: parent.width text: qsTr("Force gamepad #1 always connected") font.pointSize: 12 checked: !StreamingPreferences.multiController onCheckedChanged: { StreamingPreferences.multiController = !checked } ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: qsTr("Forces a single gamepad to always stay connected to the host, even if no gamepads are actually connected to this PC.") + " " + qsTr("Only enable this option when streaming a game that doesn't support gamepads being connected after startup.") } CheckBox { id: gamepadMouseCheck hoverEnabled: true width: parent.width text: qsTr("Enable mouse control with gamepads by holding the 'Start' button") font.pointSize: 12 checked: StreamingPreferences.gamepadMouse onCheckedChanged: { StreamingPreferences.gamepadMouse = checked } } CheckBox { id: backgroundGamepadCheck width: parent.width text: qsTr("Process gamepad input when Moonlight is in the background") font.pointSize: 12 visible: SystemProperties.hasDesktopEnvironment checked: StreamingPreferences.backgroundGamepad onCheckedChanged: { StreamingPreferences.backgroundGamepad = checked } ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: qsTr("Allows Moonlight to capture gamepad inputs even if it's not the current window in focus") } } } GroupBox { id: advancedSettingsGroupBox width: (parent.width - (parent.leftPadding + parent.rightPadding)) padding: 12 title: "" + qsTr("Advanced Settings") + "" font.pointSize: 12 Column { anchors.fill: parent spacing: 5 Label { width: parent.width id: resVDSTitle text: qsTr("Video decoder") font.pointSize: 12 wrapMode: Text.Wrap } AutoResizingComboBox { // ignore setting the index at first, and actually set it when the component is loaded Component.onCompleted: { var saved_vds = StreamingPreferences.videoDecoderSelection currentIndex = 0 for (var i = 0; i < decoderListModel.count; i++) { var el_vds = decoderListModel.get(i).val; if (saved_vds === el_vds) { currentIndex = i break } } activated(currentIndex) } id: decoderComboBox textRole: "text" model: ListModel { id: decoderListModel ListElement { text: qsTr("Automatic (Recommended)") val: StreamingPreferences.VDS_AUTO } ListElement { text: qsTr("Force software decoding") val: StreamingPreferences.VDS_FORCE_SOFTWARE } ListElement { text: qsTr("Force hardware decoding") val: StreamingPreferences.VDS_FORCE_HARDWARE } } // ::onActivated must be used, as it only listens for when the index is changed by a human onActivated: { if (enabled) { StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val } } } Label { width: parent.width id: resVCCTitle text: qsTr("Video codec") font.pointSize: 12 wrapMode: Text.Wrap } AutoResizingComboBox { // ignore setting the index at first, and actually set it when the component is loaded Component.onCompleted: { var saved_vcc = StreamingPreferences.videoCodecConfig // Default to Automatic (relevant if HDR is enabled, // where we will match none of the codecs in the list) currentIndex = 0 for(var i = 0; i < codecListModel.count; i++) { var el_vcc = codecListModel.get(i).val; if (saved_vcc === el_vcc) { currentIndex = i break } } activated(currentIndex) } id: codecComboBox textRole: "text" model: ListModel { id: codecListModel ListElement { text: qsTr("Automatic (Recommended)") val: StreamingPreferences.VCC_AUTO } ListElement { text: qsTr("H.264") val: StreamingPreferences.VCC_FORCE_H264 } ListElement { text: qsTr("HEVC (H.265)") val: StreamingPreferences.VCC_FORCE_HEVC } ListElement { text: qsTr("AV1 (Experimental)") val: StreamingPreferences.VCC_FORCE_AV1 } } // ::onActivated must be used, as it only listens for when the index is changed by a human onActivated : { if (enabled) { StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val } } } CheckBox { id: enableHdr width: parent.width text: qsTr("Enable HDR (Experimental)") font.pointSize: 12 enabled: SystemProperties.supportsHdr checked: enabled && StreamingPreferences.enableHdr onCheckedChanged: { StreamingPreferences.enableHdr = checked } // Updating StreamingPreferences.videoCodecConfig is handled above ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: enabled ? qsTr("The stream will be HDR-capable, but some games may require an HDR monitor on your host PC to enable HDR mode.") : qsTr("HDR streaming is not supported on this PC.") } CheckBox { id: enableYUV444 width: parent.width text: qsTr("Enable YUV 4:4:4 (Experimental)") font.pointSize: 12 checked: StreamingPreferences.enableYUV444 onCheckedChanged: { // This is called on init, so only reset to default bitrate when checked state changes. if (StreamingPreferences.enableYUV444 != checked) { StreamingPreferences.enableYUV444 = checked StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.height, StreamingPreferences.fps, StreamingPreferences.enableYUV444); slider.value = StreamingPreferences.bitrateKbps } } ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: enabled ? qsTr("Good for streaming desktop and text-heavy games, but not recommended for fast-paced games.") : qsTr("YUV 4:4:4 is not supported on this PC.") } CheckBox { id: unlockBitrate width: parent.width text: qsTr("Unlock bitrate limit (Experimental)") font.pointSize: 12 checked: StreamingPreferences.unlockBitrate onCheckedChanged: { StreamingPreferences.unlockBitrate = checked StreamingPreferences.bitrateKbps = Math.min(StreamingPreferences.bitrateKbps, slider.to) slider.value = StreamingPreferences.bitrateKbps } ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: qsTr("This unlocks extremely high video bitrates for use with Sunshine hosts. It should only be used when streaming over an Ethernet LAN connection.") } CheckBox { id: enableMdns width: parent.width text: qsTr("Automatically find PCs on the local network (Recommended)") font.pointSize: 12 checked: StreamingPreferences.enableMdns onCheckedChanged: { // This is called on init, so only do the work if we've // actually changed the value. if (StreamingPreferences.enableMdns != checked) { StreamingPreferences.enableMdns = checked // Restart polling so the mDNS change takes effect if (window.pollingActive) { ComputerManager.stopPollingAsync() ComputerManager.startPolling() } } } } CheckBox { id: detectNetworkBlocking width: parent.width text: qsTr("Automatically detect blocked connections (Recommended)") font.pointSize: 12 checked: StreamingPreferences.detectNetworkBlocking onCheckedChanged: { StreamingPreferences.detectNetworkBlocking = checked } } CheckBox { id: showPerformanceOverlay width: parent.width text: qsTr("Show performance stats while streaming") font.pointSize: 12 checked: StreamingPreferences.showPerformanceOverlay onCheckedChanged: { StreamingPreferences.showPerformanceOverlay = checked } ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.visible: hovered ToolTip.text: qsTr("Display real-time stream performance information while streaming.") + "\n\n" + qsTr("You can toggle it at any time while streaming using Ctrl+Alt+Shift+S or Select+L1+R1+X.") + "\n\n" + qsTr("The performance overlay is not supported on Steam Link or Raspberry Pi.") } } } } }