moonlight-qt/app/gui/SettingsView.qml
2023-06-20 21:58:10 -05:00

1517 lines
68 KiB
QML

import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.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
}
}
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: "<font color=\"skyblue\">" + qsTr("Basic Settings") + "</font>"
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
// 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 resolutions for all attached displays
var done = false
for (var displayIndex = 0; !done; displayIndex++) {
var screenRect = SystemProperties.getNativeResolution(displayIndex);
if (screenRect.width === 0) {
// Exceeded max count of displays
done = true
break
}
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 (screenRect.width === existing_width && screenRect.height === existing_height) {
// Duplicate entry, skip
indexToAdd = -1
break
}
else if (screenRect.width * screenRect.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": "Native ("+screenRect.width+"x"+screenRect.height+")",
"video_width": ""+screenRect.width,
"video_height": ""+screenRect.height,
"is_custom": false
})
}
}
// 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": "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": "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);
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 {
function addRefreshRateOrdered(fpsListModel, refreshRate, description) {
var indexToAdd = 0
for (var j = 0; j < fpsListModel.count; j++) {
var existing_fps = parseInt(fpsListModel.get(j).video_fps);
if (refreshRate === existing_fps) {
// Duplicate entry, skip
indexToAdd = -1
break
}
else if (refreshRate > existing_fps) {
// Candidate entrypoint after this entry
indexToAdd = j + 1
}
}
// Insert this display's resolution if it's not a duplicate
if (indexToAdd >= 0) {
fpsListModel.insert(indexToAdd,
{
"text": description,
"video_fps": ""+refreshRate
})
}
return indexToAdd
}
function createModel() {
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
// Default entries
fpsListModel.append({"text": qsTr("%1 FPS").arg("30"), "video_fps": "30"})
fpsListModel.append({"text": qsTr("%1 FPS").arg("60"), "video_fps": "60"})
// 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))
}
// Add unsupported FPS values
if (StreamingPreferences.unsupportedFps) {
addRefreshRateOrdered(fpsListModel, 90, qsTr("%1 FPS (Unsupported)").arg(90))
addRefreshRateOrdered(fpsListModel, 120, qsTr("%1 FPS (Unsupported)").arg(120))
}
return fpsListModel
}
function reinitialize() {
model = createModel()
var saved_fps = StreamingPreferences.fps
currentIndex = -1
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
break
}
}
// If we didn't find one, add a custom frame rate for the current value
if (currentIndex === -1) {
currentIndex = addRefreshRateOrdered(model, saved_fps, qsTr("%1 FPS (Custom)").arg(saved_fps))
}
// Persist the selected value
activated(currentIndex)
}
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
reinitialize()
languageChanged.connect(reinitialize)
}
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 : {
var selectedFps = parseInt(model.get(currentIndex).video_fps)
// Only modify the bitrate if the values actually changed
if (StreamingPreferences.fps !== selectedFps) {
StreamingPreferences.fps = selectedFps
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
StreamingPreferences.height,
StreamingPreferences.fps);
slider.value = StreamingPreferences.bitrateKbps
}
}
}
}
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: 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
}
}
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: "<font color=\"skyblue\">" + qsTr("Audio Settings") + "</font>"
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: uiSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding))
padding: 12
title: "<font color=\"skyblue\">" + qsTr("UI Settings") + "</font>"
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
}
}
// ::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: "<font color=\"skyblue\">" + qsTr("Input Settings") + "</font>"
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: "<font color=\"skyblue\">" + qsTr("Gamepad Settings") + "</font>"
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: {
// Check if the value changed (this is called on init too)
if (StreamingPreferences.swapFaceButtons !== checked) {
StreamingPreferences.swapFaceButtons = checked
// Save and restart SdlGamepadKeyNavigation so it can pull the new value
StreamingPreferences.save()
SdlGamepadKeyNavigation.disable()
SdlGamepadKeyNavigation.enable()
}
}
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: hostSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding))
padding: 12
title: "<font color=\"skyblue\">" + qsTr("Host Settings") + "</font>"
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: advancedSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding))
padding: 12
title: "<font color=\"skyblue\">" + qsTr("Advanced Settings") + "</font>"
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"
enabled: !enableHdr.checked
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
}
}
// This handles the state of the enableHdr checkbox changing
onEnabledChanged: {
if (enabled) {
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val
}
else {
StreamingPreferences.videoDecoderSelection = StreamingPreferences.VDS_AUTO
}
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered && !enabled
ToolTip.text: qsTr("Enabling HDR overrides manual decoder selections.")
}
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"
enabled: !enableHdr.checked
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
}
}
// ::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
}
}
// This handles the state of the enableHdr checkbox changing
onEnabledChanged: {
if (enabled) {
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
}
else {
StreamingPreferences.videoCodecConfig = StreamingPreferences.VCC_FORCE_HEVC_HDR
}
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered && !enabled
ToolTip.text: qsTr("Enabling HDR overrides manual codec selections.")
}
CheckBox {
id: enableHdr
width: parent.width
text: qsTr("Enable HDR (Experimental)")
font.pointSize: 12
enabled: SystemProperties.supportsHdr
checked: enabled && StreamingPreferences.videoCodecConfig == StreamingPreferences.VCC_FORCE_HEVC_HDR
// 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: unlockUnsupportedFps
width: parent.width
text: qsTr("Unlock unsupported FPS options")
font.pointSize: 12
checked: StreamingPreferences.unsupportedFps
onCheckedChanged: {
// This is called on init, so only do the work if we've
// actually changed the value.
if (StreamingPreferences.unsupportedFps != checked) {
StreamingPreferences.unsupportedFps = checked
// The selectable FPS values depend on whether
// this option is enabled or not
fpsComboBox.reinitialize()
}
}
}
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
// We must save the updated preference to ensure
// ComputerManager can observe the change internally.
StreamingPreferences.save()
// 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: {
// This is called on init, so only do the work if we've
// actually changed the value.
if (StreamingPreferences.detectNetworkBlocking != checked) {
StreamingPreferences.detectNetworkBlocking = checked
// We must save the updated preference to ensure
// ComputerManager can observe the change internally.
StreamingPreferences.save()
}
}
}
}
}
}
}