mirror of
https://github.com/moonlight-stream/moonlight-qt
synced 2025-01-09 09:48:43 +00:00
d1ccd19fcc
This removes the need for several hacks in SettingsView to force updates and improves performance by not reloading preferences all over the place.
1638 lines
73 KiB
QML
1638 lines
73 KiB
QML
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: "<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 {
|
|
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);
|
|
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: 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: "<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
|
|
}
|
|
/* ListElement {
|
|
text: "עִבְרִית" // Hebrew
|
|
val: StreamingPreferences.LANG_HE
|
|
} */
|
|
}
|
|
// ::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: {
|
|
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: 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"
|
|
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: 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|