diff --git a/app/backend/computermanager.cpp b/app/backend/computermanager.cpp index 879ce63f..96f11969 100644 --- a/app/backend/computermanager.cpp +++ b/app/backend/computermanager.cpp @@ -68,12 +68,7 @@ private: } QWriteLocker lock(&m_Computer->lock); - if (m_Computer->appList != appList) { - m_Computer->appList = appList; - m_Computer->sortAppList(); - changed = true; - } - + changed = m_Computer->updateAppList(appList); return true; } @@ -433,6 +428,15 @@ void ComputerManager::renameHost(NvComputer* computer, QString name) handleComputerStateChanged(computer); } +void ComputerManager::clientSideAttributeUpdated(NvComputer* computer) +{ + // Persist the change + saveHosts(); + + // Notify the UI of the state change + handleComputerStateChanged(computer); +} + void ComputerManager::handleAboutToQuit() { QWriteLocker lock(&m_Lock); diff --git a/app/backend/computermanager.h b/app/backend/computermanager.h index d9d1c976..08675669 100644 --- a/app/backend/computermanager.h +++ b/app/backend/computermanager.h @@ -181,6 +181,8 @@ public: void renameHost(NvComputer* computer, QString name); + void clientSideAttributeUpdated(NvComputer* computer); + signals: void computerStateChanged(NvComputer* computer); diff --git a/app/backend/nvapp.cpp b/app/backend/nvapp.cpp index f06fca13..e3f866f2 100644 --- a/app/backend/nvapp.cpp +++ b/app/backend/nvapp.cpp @@ -4,6 +4,7 @@ #define SER_APPID "id" #define SER_APPHDR "hdr" #define SER_APPCOLLECTOR "appcollector" +#define SER_HIDDEN "hidden" NvApp::NvApp(QSettings& settings) { @@ -11,6 +12,7 @@ NvApp::NvApp(QSettings& settings) id = settings.value(SER_APPID).toInt(); hdrSupported = settings.value(SER_APPHDR).toBool(); isAppCollectorGame = settings.value(SER_APPCOLLECTOR).toBool(); + hidden = settings.value(SER_HIDDEN).toBool(); } void NvApp::serialize(QSettings& settings) const @@ -19,4 +21,5 @@ void NvApp::serialize(QSettings& settings) const settings.setValue(SER_APPID, id); settings.setValue(SER_APPHDR, hdrSupported); settings.setValue(SER_APPCOLLECTOR, isAppCollectorGame); + settings.setValue(SER_HIDDEN, hidden); } diff --git a/app/backend/nvapp.h b/app/backend/nvapp.h index bc70f37a..ef1c9a9b 100644 --- a/app/backend/nvapp.h +++ b/app/backend/nvapp.h @@ -10,7 +10,16 @@ public: bool operator==(const NvApp& other) const { - return id == other.id; + return id == other.id && + name == other.name && + hdrSupported == other.hdrSupported && + isAppCollectorGame == other.isAppCollectorGame && + hidden == other.hidden; + } + + bool operator!=(const NvApp& other) const + { + return !operator==(other); } bool isInitialized() @@ -25,6 +34,7 @@ public: QString name; bool hdrSupported = false; bool isAppCollectorGame = false; + bool hidden = false; }; Q_DECLARE_METATYPE(NvApp) diff --git a/app/backend/nvcomputer.cpp b/app/backend/nvcomputer.cpp index 8783eee6..6191cdb1 100644 --- a/app/backend/nvcomputer.cpp +++ b/app/backend/nvcomputer.cpp @@ -306,6 +306,25 @@ bool NvComputer::isReachableOverVpn() } } +bool NvComputer::updateAppList(QVector newAppList) { + if (appList == newAppList) { + return false; + } + + // Propagate client-side attributes to the new app list + for (const NvApp& existingApp : appList) { + for (NvApp& newApp : newAppList) { + if (existingApp.id == newApp.id) { + newApp.hidden = existingApp.hidden; + } + } + } + + appList = newAppList; + sortAppList(); + return true; +} + QVector NvComputer::uniqueAddresses() const { QVector uniqueAddressList; @@ -389,7 +408,12 @@ bool NvComputer::update(NvComputer& that) ASSIGN_IF_CHANGED(maxLumaPixelsHEVC); ASSIGN_IF_CHANGED(gpuModel); ASSIGN_IF_CHANGED_AND_NONNULL(serverCert); - ASSIGN_IF_CHANGED_AND_NONEMPTY(appList); ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes); + + if (!that.appList.isEmpty()) { + // updateAppList() handles merging client-side attributes + updateAppList(that.appList); + } + return changed; } diff --git a/app/backend/nvcomputer.h b/app/backend/nvcomputer.h index da268013..d66ee5ec 100644 --- a/app/backend/nvcomputer.h +++ b/app/backend/nvcomputer.h @@ -16,6 +16,8 @@ class NvComputer private: void sortAppList(); + bool updateAppList(QVector newAppList); + bool pendingQuit; public: diff --git a/app/gui/AppView.qml b/app/gui/AppView.qml index fd34fe1d..6397f313 100644 --- a/app/gui/AppView.qml +++ b/app/gui/AppView.qml @@ -9,6 +9,7 @@ CenteredGridView { property int computerIndex property AppModel appModel : createModel() property bool activated + property bool showHiddenGames id: appGrid focus: true @@ -48,7 +49,7 @@ CenteredGridView { function createModel() { var model = Qt.createQmlObject('import AppModel 1.0; AppModel {}', parent, '') - model.initialize(ComputerManager, computerIndex) + model.initialize(ComputerManager, computerIndex, showHiddenGames) return model } @@ -58,6 +59,9 @@ CenteredGridView { width: 220; height: 287; grid: appGrid + // Dim the app if it's hidden + opacity: model.hidden ? 0.4 : 1.0 + Image { property bool isPlaceholder: false @@ -165,8 +169,8 @@ CenteredGridView { function launchOrResumeSelectedApp() { - var runningIndex = appModel.getRunningAppIndex() - if (runningIndex >= 0 && runningIndex !== index) { + var runningId = appModel.getRunningAppId() + if (runningId !== 0 && runningId !== model.appid) { quitAppDialog.appName = appModel.getRunningAppName() quitAppDialog.segueToStream = true quitAppDialog.nextAppName = model.name @@ -190,6 +194,25 @@ CenteredGridView { } } + onPressAndHold: { + // popup() ensures the menu appears under the mouse cursor + if (appContextMenu.popup) { + appContextMenu.popup() + } + else { + // Qt 5.9 doesn't have popup() + appContextMenu.open() + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton; + onClicked: { + parent.onPressAndHold() + } + } + Keys.onReturnPressed: { // Open the app context menu if activated via the gamepad or keyboard // for running games. If the game isn't running, the above onClicked @@ -228,6 +251,14 @@ CenteredGridView { onTriggered: doQuitGame() visible: model.running } + NavigableMenuItem { + parentMenu: appContextMenu + checkable: true + checked: model.hidden + text: "Hide Game" + onTriggered: appModel.setAppHidden(model.index, !model.hidden) + visible: !model.running || model.hidden + } } } diff --git a/app/gui/PcView.qml b/app/gui/PcView.qml index 6c42b2e1..bb4568dc 100644 --- a/app/gui/PcView.qml +++ b/app/gui/PcView.qml @@ -151,6 +151,26 @@ CenteredGridView { NavigableMenu { id: pcContextMenu + NavigableMenuItem { + parentMenu: pcContextMenu + text: "View Apps" + onTriggered: { + var component = Qt.createComponent("AppView.qml") + var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name}) + stackView.push(appView) + } + visible: model.online && model.paired + } + NavigableMenuItem { + parentMenu: pcContextMenu + text: "View Hidden Apps" + onTriggered: { + var component = Qt.createComponent("AppView.qml") + var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true}) + stackView.push(appView) + } + visible: model.online && model.paired + } NavigableMenuItem { parentMenu: pcContextMenu text: "Delete PC" diff --git a/app/gui/appmodel.cpp b/app/gui/appmodel.cpp index 2c23185c..2325ba84 100644 --- a/app/gui/appmodel.cpp +++ b/app/gui/appmodel.cpp @@ -7,7 +7,7 @@ AppModel::AppModel(QObject *parent) this, &AppModel::handleBoxArtLoaded); } -void AppModel::initialize(ComputerManager* computerManager, int computerIndex) +void AppModel::initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames) { m_ComputerManager = computerManager; connect(m_ComputerManager, &ComputerManager::computerStateChanged, @@ -15,29 +15,23 @@ void AppModel::initialize(ComputerManager* computerManager, int computerIndex) Q_ASSERT(computerIndex < m_ComputerManager->getComputers().count()); m_Computer = m_ComputerManager->getComputers().at(computerIndex); - m_Apps = m_Computer->appList; m_CurrentGameId = m_Computer->currentGameId; + m_ShowHiddenGames = showHiddenGames; + + updateAppList(m_Computer->appList); } -int AppModel::getRunningAppIndex() +int AppModel::getRunningAppId() { - if (m_CurrentGameId != 0) { - for (int i = 0; i < m_Apps.count(); i++) { - if (m_Apps[i].id == m_CurrentGameId) { - return i; - } - } - } - - return -1; + return m_CurrentGameId; } QString AppModel::getRunningAppName() { if (m_CurrentGameId != 0) { - for (int i = 0; i < m_Apps.count(); i++) { - if (m_Apps[i].id == m_CurrentGameId) { - return m_Apps[i].name; + for (int i = 0; i < m_AllApps.count(); i++) { + if (m_AllApps[i].id == m_CurrentGameId) { + return m_AllApps[i].name; } } } @@ -47,8 +41,8 @@ QString AppModel::getRunningAppName() Session* AppModel::createSessionForApp(int appIndex) { - Q_ASSERT(appIndex < m_Apps.count()); - NvApp app = m_Apps.at(appIndex); + Q_ASSERT(appIndex < m_VisibleApps.count()); + NvApp app = m_VisibleApps.at(appIndex); return new Session(m_Computer, app); } @@ -60,7 +54,7 @@ int AppModel::rowCount(const QModelIndex &parent) const if (parent.isValid()) return 0; - return m_Apps.count(); + return m_VisibleApps.count(); } QVariant AppModel::data(const QModelIndex &index, int role) const @@ -68,8 +62,8 @@ QVariant AppModel::data(const QModelIndex &index, int role) const if (!index.isValid()) return QVariant(); - Q_ASSERT(index.row() < m_Apps.count()); - NvApp app = m_Apps.at(index.row()); + Q_ASSERT(index.row() < m_VisibleApps.count()); + NvApp app = m_VisibleApps.at(index.row()); switch (role) { @@ -80,6 +74,10 @@ QVariant AppModel::data(const QModelIndex &index, int role) const case BoxArtRole: // FIXME: const-correctness return const_cast(m_BoxArtManager).loadBoxArt(m_Computer, app); + case HiddenRole: + return app.hidden; + case AppIdRole: + return app.id; default: return QVariant(); } @@ -92,6 +90,8 @@ QHash AppModel::roleNames() const names[NameRole] = "name"; names[RunningRole] = "running"; names[BoxArtRole] = "boxart"; + names[HiddenRole] = "hidden"; + names[AppIdRole] = "appid"; return names; } @@ -101,6 +101,98 @@ void AppModel::quitRunningApp() m_ComputerManager->quitRunningApp(m_Computer); } +QVector AppModel::getVisibleApps(const QVector& appList) +{ + QVector visibleApps; + + for (const NvApp& app : appList) { + if (m_ShowHiddenGames || !app.hidden) { + visibleApps.append(app); + } + } + + return visibleApps; +} + +void AppModel::updateAppList(QVector newList) +{ + m_AllApps = newList; + + QVector newVisibleList = getVisibleApps(newList); + + // Process removals and updates first + for (int i = 0; i < m_VisibleApps.count(); i++) { + const NvApp& existingApp = m_VisibleApps.at(i); + + bool found = false; + for (const NvApp& newApp : newVisibleList) { + if (existingApp.id == newApp.id) { + // If the data changed, update it in our list + if (existingApp != newApp) { + m_VisibleApps.replace(i, newApp); + emit dataChanged(createIndex(i, 0), createIndex(i, 0)); + } + + found = true; + break; + } + } + + if (!found) { + beginRemoveRows(QModelIndex(), i, i); + m_VisibleApps.removeAt(i); + endRemoveRows(); + i--; + } + } + + // Process additions now + for (const NvApp& newApp : newVisibleList) { + int insertionIndex = m_VisibleApps.size(); + bool found = false; + + for (int i = 0; i < m_VisibleApps.count(); i++) { + const NvApp& existingApp = m_VisibleApps.at(i); + + if (existingApp.id == newApp.id) { + found = true; + break; + } + else if (existingApp.name.toLower() > newApp.name.toLower()) { + insertionIndex = i; + break; + } + } + + if (!found) { + beginInsertRows(QModelIndex(), insertionIndex, insertionIndex); + m_VisibleApps.insert(insertionIndex, newApp); + endInsertRows(); + } + } + + Q_ASSERT(newVisibleList == m_VisibleApps); +} + +void AppModel::setAppHidden(int appIndex, bool hidden) +{ + Q_ASSERT(appIndex < m_VisibleApps.count()); + int appId = m_VisibleApps.at(appIndex).id; + + { + QWriteLocker lock(&m_Computer->lock); + + for (NvApp& app : m_Computer->appList) { + if (app.id == appId) { + app.hidden = hidden; + break; + } + } + } + + m_ComputerManager->clientSideAttributeUpdated(m_Computer); +} + void AppModel::handleComputerStateChanged(NvComputer* computer) { // Ignore updates for computers that aren't ours @@ -119,20 +211,15 @@ void AppModel::handleComputerStateChanged(NvComputer* computer) // First, process additions/removals from the app list. This // is required because the new game may now be running, so // we can't check that first. - if (computer->appList != m_Apps) { - // Just reset the whole thing if the list changes - beginResetModel(); - m_Apps = computer->appList; - m_CurrentGameId = computer->currentGameId; - endResetModel(); - return; + if (computer->appList != m_AllApps) { + updateAppList(computer->appList); } // Finally, process changes to the active app if (computer->currentGameId != m_CurrentGameId) { // First, invalidate the running state of newly running game - for (int i = 0; i < m_Apps.count(); i++) { - if (m_Apps[i].id == computer->currentGameId) { + for (int i = 0; i < m_VisibleApps.count(); i++) { + if (m_VisibleApps[i].id == computer->currentGameId) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), QVector() << RunningRole); @@ -142,8 +229,8 @@ void AppModel::handleComputerStateChanged(NvComputer* computer) // Next, invalidate the running state of the old game (if it exists) if (m_CurrentGameId != 0) { - for (int i = 0; i < m_Apps.count(); i++) { - if (m_Apps[i].id == m_CurrentGameId) { + for (int i = 0; i < m_VisibleApps.count(); i++) { + if (m_VisibleApps[i].id == m_CurrentGameId) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), QVector() << RunningRole); @@ -161,7 +248,7 @@ void AppModel::handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl /* image { Q_ASSERT(computer == m_Computer); - int index = m_Apps.indexOf(app); + int index = m_VisibleApps.indexOf(app); // Make sure we're not delivering a callback to an app that's already been removed if (index >= 0) { diff --git a/app/gui/appmodel.h b/app/gui/appmodel.h index 5f07c86d..a69abd7a 100644 --- a/app/gui/appmodel.h +++ b/app/gui/appmodel.h @@ -14,23 +14,27 @@ class AppModel : public QAbstractListModel { NameRole = Qt::UserRole, RunningRole, - BoxArtRole + BoxArtRole, + HiddenRole, + AppIdRole, }; public: explicit AppModel(QObject *parent = nullptr); // Must be called before any QAbstractListModel functions - Q_INVOKABLE void initialize(ComputerManager* computerManager, int computerIndex); + Q_INVOKABLE void initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames); Q_INVOKABLE Session* createSessionForApp(int appIndex); - Q_INVOKABLE int getRunningAppIndex(); + Q_INVOKABLE int getRunningAppId(); Q_INVOKABLE QString getRunningAppName(); Q_INVOKABLE void quitRunningApp(); + Q_INVOKABLE void setAppHidden(int appIndex, bool hidden); + QVariant data(const QModelIndex &index, int role) const override; int rowCount(const QModelIndex &parent) const override; @@ -46,9 +50,14 @@ signals: void computerLost(); private: + void updateAppList(QVector newList); + + QVector getVisibleApps(const QVector& appList); + NvComputer* m_Computer; BoxArtManager m_BoxArtManager; ComputerManager* m_ComputerManager; - QVector m_Apps; + QVector m_VisibleApps, m_AllApps; int m_CurrentGameId; + bool m_ShowHiddenGames; };