moonlight-qt/app/gui/main.qml

532 lines
18 KiB
QML
Raw Normal View History

2018-07-04 21:16:25 +00:00
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
2018-07-04 21:16:25 +00:00
import ComputerManager 1.0
import AutoUpdateChecker 1.0
import StreamingPreferences 1.0
import SystemProperties 1.0
import SdlGamepadKeyNavigation 1.0
2018-07-04 21:16:25 +00:00
ApplicationWindow {
property bool pollingActive: false
// Set by SettingsView to force the back operation to pop all
// pages except the initial view. This is required when doing
// a retranslate() because AppView breaks for some reason.
property bool clearOnBack: false
2018-07-04 21:16:25 +00:00
id: window
visible: true
width: 1280
height: 600
2018-07-09 06:24:26 +00:00
visibility: {
if (SystemProperties.hasDesktopEnvironment) {
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) return "Maximized"
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) return "FullScreen"
} else {
return "FullScreen"
}
}
2020-12-27 21:34:49 +00:00
// This configures the maximum width of the singleton attached QML ToolTip. If left unconstrained,
// it will never insert a line break and just extend on forever.
ToolTip.toolTip.contentWidth: ToolTip.toolTip.implicitContentWidth < 400 ? ToolTip.toolTip.implicitContentWidth : 400
function goBack() {
if (clearOnBack) {
// Pop all items except the first one
stackView.pop(null)
clearOnBack = false
}
else {
stackView.pop()
}
}
StackView {
id: stackView
initialItem: initialView
anchors.fill: parent
focus: true
onCurrentItemChanged: {
// Ensure focus travels to the next view when going back
if (currentItem) {
currentItem.forceActiveFocus()
}
}
Keys.onEscapePressed: {
if (depth > 1) {
goBack()
}
else {
quitConfirmationDialog.open()
}
}
Keys.onBackPressed: {
if (depth > 1) {
goBack()
}
else {
quitConfirmationDialog.open()
}
}
Keys.onMenuPressed: {
settingsButton.clicked()
}
// This is a keypress we've reserved for letting the
// SdlGamepadKeyNavigation object tell us to show settings
// when Menu is consumed by a focused control.
Keys.onHangupPressed: {
settingsButton.clicked()
}
2018-07-04 21:16:25 +00:00
}
// This timer keeps us polling for 5 minutes of inactivity
// to allow the user to work with Moonlight on a second display
// while dealing with configuration issues. This will ensure
// machines come online even if the input focus isn't on Moonlight.
Timer {
id: inactivityTimer
interval: 5 * 60000
onTriggered: {
if (!active && pollingActive) {
ComputerManager.stopPollingAsync()
pollingActive = false
}
}
}
onVisibleChanged: {
// When we become invisible while streaming is going on,
// stop polling immediately.
if (!visible) {
inactivityTimer.stop()
if (pollingActive) {
ComputerManager.stopPollingAsync()
pollingActive = false
}
}
else if (active) {
// When we become visible and active again, start polling
inactivityTimer.stop()
// Restart polling if it was stopped
if (!pollingActive) {
ComputerManager.startPolling()
pollingActive = true
}
}
}
onActiveChanged: {
if (active) {
// Stop the inactivity timer
inactivityTimer.stop()
// Restart polling if it was stopped
if (!pollingActive) {
ComputerManager.startPolling()
pollingActive = true
}
}
else {
// Start the inactivity timer to stop polling
// if focus does not return within a few minutes.
inactivityTimer.restart()
}
}
property bool initialized: false
// BUG: Using onAfterSynchronizing: here causes very strange
// failures on Linux. Many shaders fail to compile and we
// eventually segfault deep inside the Qt OpenGL code.
onAfterRendering: {
// We use this callback to trigger dialog display because
// it only happens once the window is fully constructed.
// Doing it earlier can lead to the dialog appearing behind
// the window or otherwise without input focus.
if (!initialized) {
// Set initialized before calling anything else, because
// pumping the event loop can cause us to get another
// onAfterRendering call and potentially reenter this code.
initialized = true;
if (SystemProperties.isWow64) {
wow64Dialog.open()
}
else if (!SystemProperties.hasHardwareAcceleration) {
if (SystemProperties.isRunningXWayland) {
xWaylandDialog.open()
}
else {
noHwDecoderDialog.open()
}
}
if (SystemProperties.unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
unmappedGamepadDialog.open()
}
}
}
// Workaround for lack of instanceof in Qt 5.9.
//
// Based on https://stackoverflow.com/questions/13923794/how-to-do-a-is-a-typeof-or-instanceof-in-qml
function qmltypeof(obj, className) { // QtObject, string -> bool
// className plus "(" is the class instance without modification
// className plus "_QML" is the class instance with user-defined properties
var str = obj.toString();
return str.startsWith(className + "(") || str.startsWith(className + "_QML");
}
function navigateTo(url, objectType)
{
var existingItem = stackView.find(function(item, index) {
return qmltypeof(item, objectType)
})
if (existingItem !== null) {
// Pop to the existing item
stackView.pop(existingItem)
}
else {
// Create a new item
stackView.push(url)
}
}
header: ToolBar {
id: toolBar
height: 60
2018-08-31 06:28:26 +00:00
anchors.topMargin: 5
anchors.bottomMargin: 5
2020-05-10 18:48:24 +00:00
Label {
id: titleLabel
visible: toolBar.width > 700
2020-05-10 18:48:24 +00:00
anchors.fill: parent
text: stackView.currentItem.objectName
font.pointSize: 20
elide: Label.ElideRight
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
}
RowLayout {
spacing: 10
anchors.leftMargin: 10
anchors.rightMargin: 10
anchors.fill: parent
NavigableToolButton {
// Only make the button visible if the user has navigated somewhere.
visible: stackView.depth > 1
iconSource: "qrc:/res/arrow_left.svg"
2018-08-31 06:28:26 +00:00
onClicked: goBack()
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
// This label will appear when the window gets too small and
// we need to ensure the toolbar controls don't collide
Label {
id: titleRowLabel
font.pointSize: titleLabel.font.pointSize
elide: Label.ElideRight
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
Layout.fillWidth: true
// We need this label to always be visible so it can occupy
// the remaining space in the RowLayout. To "hide" it, we
// just set the text to empty string.
text: !titleLabel.visible ? stackView.currentItem.objectName : ""
}
Label {
id: versionLabel
visible: qmltypeof(stackView.currentItem, "SettingsView")
2020-11-21 19:15:54 +00:00
text: qsTr("Version %1").arg(SystemProperties.versionString)
2020-05-10 06:02:09 +00:00
font.pointSize: 12
horizontalAlignment: Qt.AlignRight
verticalAlignment: Qt.AlignVCenter
}
2020-09-10 04:49:45 +00:00
NavigableToolButton {
id: discordButton
visible: SystemProperties.hasBrowser &&
qmltypeof(stackView.currentItem, "SettingsView")
2020-09-10 04:49:45 +00:00
iconSource: "qrc:/res/Discord-Logo-White.svg"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: qsTr("Join our community on Discord")
2020-09-10 04:49:45 +00:00
// TODO need to make sure browser is brought to foreground.
onClicked: Qt.openUrlExternally("https://moonlight-stream.org/discord");
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
2019-03-27 04:31:51 +00:00
NavigableToolButton {
id: addPcButton
visible: qmltypeof(stackView.currentItem, "PcView")
2019-03-27 04:31:51 +00:00
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
2019-03-27 04:31:51 +00:00
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: qsTr("Add PC manually") + (newPcShortcut.nativeText ? (" ("+newPcShortcut.nativeText+")") : "")
Shortcut {
id: newPcShortcut
sequence: StandardKey.New
onActivated: addPcButton.clicked()
}
2019-03-27 04:31:51 +00:00
onClicked: {
addPcDialog.open()
}
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
NavigableToolButton {
property string browserUrl: ""
id: updateButton
2018-08-31 06:28:26 +00:00
iconSource: "qrc:/res/update.svg"
2018-08-10 06:20:20 +00:00
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered || visible
2018-08-10 06:20:20 +00:00
// Invisible until we get a callback notifying us that
// an update is available
visible: false
2019-12-14 04:15:52 +00:00
onClicked: {
if (SystemProperties.hasBrowser) {
Qt.openUrlExternally(browserUrl);
}
}
function updateAvailable(version, url)
{
2020-11-21 19:15:54 +00:00
ToolTip.text = qsTr("Update available for Moonlight: Version %1").arg(version)
updateButton.browserUrl = url
updateButton.visible = true
}
Component.onCompleted: {
AutoUpdateChecker.onUpdateAvailable.connect(updateAvailable)
AutoUpdateChecker.start()
}
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
NavigableToolButton {
id: helpButton
visible: SystemProperties.hasBrowser
iconSource: "qrc:/res/question_mark.svg"
2018-08-10 06:20:20 +00:00
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: qsTr("Help") + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "")
Shortcut {
id: helpShortcut
sequence: StandardKey.HelpContents
onActivated: helpButton.clicked()
}
2018-08-10 06:20:20 +00:00
// TODO need to make sure browser is brought to foreground.
onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
NavigableToolButton {
// TODO: Implement gamepad mapping then unhide this button
visible: false
2018-08-10 06:20:20 +00:00
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
2020-11-21 19:15:54 +00:00
ToolTip.text: qsTr("Gamepad Mapper")
2018-08-10 06:20:20 +00:00
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
2018-08-31 06:28:26 +00:00
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "GamepadMapper")
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
NavigableToolButton {
id: settingsButton
iconSource: "qrc:/res/settings.svg"
2018-08-31 06:28:26 +00:00
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "SettingsView")
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
Shortcut {
id: settingsShortcut
sequence: StandardKey.Preferences
onActivated: settingsButton.clicked()
}
2018-08-10 06:20:20 +00:00
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: qsTr("Settings") + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "")
}
}
}
ErrorMessageDialog {
id: noHwDecoderDialog
text: qsTr("No functioning hardware accelerated video decoder was detected by Moonlight. " +
2020-11-21 19:15:54 +00:00
"Your streaming performance may be severely degraded in this configuration.")
helpText: qsTr("Click the Help button for more information on solving this problem.")
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
}
ErrorMessageDialog {
id: xWaylandDialog
2020-11-21 19:15:54 +00:00
text: qsTr("Hardware acceleration doesn't work on XWayland. Continuing on XWayland may result in poor streaming performance. " +
"Try running with QT_QPA_PLATFORM=wayland or switch to X11.")
helpText: qsTr("Click the Help button for more information.")
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
}
NavigableMessageDialog {
id: wow64Dialog
standardButtons: Dialog.Ok | Dialog.Cancel
text: qsTr("This version of Moonlight isn't optimized for your PC. Please download the '%1' version of Moonlight for the best streaming performance.").arg(SystemProperties.friendlyNativeArchName)
onAccepted: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
}
}
ErrorMessageDialog {
id: unmappedGamepadDialog
property string unmappedGamepads : ""
2020-11-21 19:15:54 +00:00
text: qsTr("Moonlight detected gamepads without a mapping:") + "\n" + unmappedGamepads
helpTextSeparator: "\n\n"
helpText: qsTr("Click the Help button for information on how to map your gamepads.")
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping"
}
// This dialog appears when quitting via keyboard or gamepad button
NavigableMessageDialog {
id: quitConfirmationDialog
standardButtons: Dialog.Yes | Dialog.No
text: qsTr("Are you sure you want to quit?")
// For keyboard/gamepad navigation
onAccepted: Qt.quit()
}
2019-03-27 04:31:51 +00:00
// HACK: This belongs in StreamSegue but keeping a dialog around after the parent
// dies can trigger bugs in Qt 5.12 that cause the app to crash. For now, we will
// host this dialog in a QML component that is never destroyed.
//
// To repro: Start a stream, cut the network connection to trigger the "Connection
// terminated" dialog, wait until the app grid times out back to the PC grid, then
// try to dismiss the dialog.
ErrorMessageDialog {
id: streamSegueErrorDialog
property bool quitAfter: false
onClosed: {
if (quitAfter) {
Qt.quit()
}
// StreamSegue assumes its dialog will be re-created each time we
// start streaming, so fake it by wiping out the text each time.
text = ""
}
}
NavigableDialog {
2019-03-27 04:31:51 +00:00
id: addPcDialog
property string label: qsTr("Enter the IP address of your GameStream PC:")
2019-03-27 04:31:51 +00:00
standardButtons: Dialog.Ok | Dialog.Cancel
2019-03-27 04:31:51 +00:00
onOpened: {
// Force keyboard focus on the textbox so keyboard navigation works
editText.forceActiveFocus()
}
onClosed: {
editText.clear()
}
2019-03-27 04:31:51 +00:00
onAccepted: {
if (editText.text) {
ComputerManager.addNewHostManually(editText.text.trim())
2019-03-27 04:31:51 +00:00
}
}
ColumnLayout {
Label {
2019-03-27 04:31:51 +00:00
text: addPcDialog.label
font.bold: true
2019-03-27 04:31:51 +00:00
}
TextField {
id: editText
Layout.fillWidth: true
focus: true
Keys.onReturnPressed: {
addPcDialog.accept()
}
Keys.onEnterPressed: {
addPcDialog.accept()
}
2019-03-27 04:31:51 +00:00
}
}
}
2018-07-04 21:16:25 +00:00
}