From c66dafbc7d9118559fb2080b71172c6d861bdd12 Mon Sep 17 00:00:00 2001 From: Casper Jeukendrup <48658420+cbjeukendrup@users.noreply.github.com> Date: Thu, 7 May 2026 01:06:14 +0200 Subject: [PATCH 1/4] Home > Scores: add context menu * Open a score * Reveal it in the file browser (local scores) / online (cloud scores) * Remove it from recent files (on the recent files page only) Resolves: https://github.com/musescore/MuseScore/issues/11235 --- .../internal/recentfilescontroller.cpp | 31 ++++++++++ src/project/internal/recentfilescontroller.h | 1 + src/project/irecentfilescontroller.h | 1 + .../qml/MuseScore/Project/ScoresPage.qml | 16 +++++ .../internal/ScoresPage/CloudScoresView.qml | 16 +++++ .../internal/ScoresPage/RecentScoresView.qml | 26 ++++++++ .../internal/ScoresPage/ScoreGridItem.qml | 59 ++++++++++++++++++- .../internal/ScoresPage/ScoreListItem.qml | 59 +++++++++++++++++++ .../internal/ScoresPage/ScoresGridView.qml | 17 ++++++ .../internal/ScoresPage/ScoresListView.qml | 20 ++++++- .../internal/ScoresPage/ScoresView.qml | 2 + .../internal/ScoresPage/recentscoresmodel.cpp | 5 ++ .../internal/ScoresPage/recentscoresmodel.h | 1 + .../internal/ScoresPage/scorespagemodel.cpp | 30 ++++++++++ .../internal/ScoresPage/scorespagemodel.h | 2 + 15 files changed, 284 insertions(+), 2 deletions(-) diff --git a/src/project/internal/recentfilescontroller.cpp b/src/project/internal/recentfilescontroller.cpp index c0abb70873ab0..26b4fd0a8e54a 100644 --- a/src/project/internal/recentfilescontroller.cpp +++ b/src/project/internal/recentfilescontroller.cpp @@ -33,6 +33,8 @@ #include "global/concurrency/concurrent.h" #endif +#include "log.h" + using namespace mu::project; using namespace muse; using namespace muse::async; @@ -99,6 +101,8 @@ void RecentFilesController::prependRecentFile(const RecentFile& newFile) void RecentFilesController::moveRecentFile(const muse::io::path_t& before, const RecentFile& after) { + TRACEFUNC; + bool moved = false; RecentFilesList newList = m_recentFilesList; @@ -115,6 +119,33 @@ void RecentFilesController::moveRecentFile(const muse::io::path_t& before, const } } +void RecentFilesController::removeRecentFile(const muse::io::path_t& path) +{ + if (path.empty()) { + return; + } + + TRACEFUNC; + + RecentFilesList newList; + newList.reserve(m_recentFilesList.size()); + + bool removed = false; + + for (const RecentFile& file : m_recentFilesList) { + if (file.path == path) { + removed = true; + continue; + } + + newList.push_back(file); + } + + if (removed) { + setRecentFilesList(newList, true); + } +} + void RecentFilesController::clearRecentFiles() { setRecentFilesList({}, true); diff --git a/src/project/internal/recentfilescontroller.h b/src/project/internal/recentfilescontroller.h index 99296e1c64c8f..b81f5cf64791b 100644 --- a/src/project/internal/recentfilescontroller.h +++ b/src/project/internal/recentfilescontroller.h @@ -51,6 +51,7 @@ class RecentFilesController : public IRecentFilesController, public muse::async: void prependRecentFile(const RecentFile& file) override; void moveRecentFile(const muse::io::path_t& before, const RecentFile& after) override; + void removeRecentFile(const muse::io::path_t& path) override; void clearRecentFiles() override; muse::async::Promise thumbnail(const muse::io::path_t& file) const override; diff --git a/src/project/irecentfilescontroller.h b/src/project/irecentfilescontroller.h index e7a860bc9c748..21d62ac91be3e 100644 --- a/src/project/irecentfilescontroller.h +++ b/src/project/irecentfilescontroller.h @@ -44,6 +44,7 @@ class IRecentFilesController : MODULE_CONTEXT_INTERFACE virtual void prependRecentFile(const RecentFile& file) = 0; virtual void moveRecentFile(const muse::io::path_t& before, const RecentFile& after) = 0; + virtual void removeRecentFile(const muse::io::path_t& path) = 0; virtual void clearRecentFiles() = 0; virtual muse::async::Promise thumbnail(const muse::io::path_t& filePath) const = 0; diff --git a/src/project/qml/MuseScore/Project/ScoresPage.qml b/src/project/qml/MuseScore/Project/ScoresPage.qml index 000a4d201d71c..38713cb50a4cf 100644 --- a/src/project/qml/MuseScore/Project/ScoresPage.qml +++ b/src/project/qml/MuseScore/Project/ScoresPage.qml @@ -261,6 +261,14 @@ FocusScope { onOpenScoreRequested: function(scorePath, displayName) { Qt.callLater(scoresPageModel.openScore, scorePath, displayName) } + + onRevealInFileBrowserRequested: function(scorePath) { + Qt.callLater(scoresPageModel.revealInFileBrowser, scorePath) + } + + onViewOnlineRequested: function(scoreId) { + Qt.callLater(scoresPageModel.viewOnline, scoreId) + } } } @@ -288,6 +296,14 @@ FocusScope { Qt.callLater(scoresPageModel.openScore, scorePath, displayName) } + onRevealInFileBrowserRequested: function(scorePath) { + Qt.callLater(scoresPageModel.revealInFileBrowser, scorePath) + } + + onViewOnlineRequested: function(scoreId) { + Qt.callLater(scoresPageModel.viewOnline, scoreId) + } + Connections { target: refreshButton diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresView.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresView.qml index 7bb59874c10ae..ca813f5f7d634 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresView.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresView.qml @@ -82,6 +82,14 @@ ScoresView { onOpenScoreRequested: function(scorePath, displayName) { root.openScoreRequested(scorePath, displayName) } + + onRevealInFileBrowserRequested: function(scorePath) { + root.revealInFileBrowserRequested(scorePath) + } + + onViewOnlineRequested: function(scoreId) { + root.viewOnlineRequested(scoreId) + } } } @@ -107,6 +115,14 @@ ScoresView { onOpenScoreRequested: function(scorePath, displayName) { root.openScoreRequested(scorePath, displayName) } + + onRevealInFileBrowserRequested: function(scorePath) { + root.revealInFileBrowserRequested(scorePath) + } + + onViewOnlineRequested: function(scoreId) { + root.viewOnlineRequested(scoreId) + } } } diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/RecentScoresView.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/RecentScoresView.qml index 6c878688a582d..f7489c1782cc7 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/RecentScoresView.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/RecentScoresView.qml @@ -47,6 +47,7 @@ ScoresView { model: recentScoresModel searchText: root.searchText + allowRemoveFromRecentFiles: true isNoResultsMessageAllowed: false // provided by the model instead @@ -65,6 +66,18 @@ ScoresView { onOpenScoreRequested: function(scorePath, displayName) { root.openScoreRequested(scorePath, displayName) } + + onRevealInFileBrowserRequested: function(scorePath) { + root.revealInFileBrowserRequested(scorePath) + } + + onViewOnlineRequested: function(scoreId) { + root.viewOnlineRequested(scoreId) + } + + onRemoveFromRecentFilesRequested: function(scorePath) { + recentScoresModel.removeRecentScore(scorePath) + } } } @@ -78,6 +91,7 @@ ScoresView { model: recentScoresModel searchText: root.searchText + allowRemoveFromRecentFiles: true backgroundColor: root.backgroundColor sideMargin: root.sideMargin @@ -97,6 +111,18 @@ ScoresView { root.openScoreRequested(scorePath, displayName) } + onRevealInFileBrowserRequested: function(scorePath) { + root.revealInFileBrowserRequested(scorePath) + } + + onViewOnlineRequested: function(scoreId) { + root.viewOnlineRequested(scoreId) + } + + onRemoveFromRecentFilesRequested: function(scorePath) { + recentScoresModel.removeRecentScore(scorePath) + } + columns: [ ScoresListView.ColumnItem { id: modifiedColumn diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreGridItem.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreGridItem.qml index 2b423909cc49e..99c0b4a7d1f08 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreGridItem.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreGridItem.qml @@ -39,10 +39,14 @@ FocusScope { property bool isNoResultsFound: false property bool isCloud: false property int cloudScoreId: 0 + property bool showRemoveFromRecentFiles: false property alias navigation: navCtrl signal clicked() + signal revealInFileBrowserRequested(string scorePath) + signal viewOnlineRequested(int scoreId) + signal removeFromRecentFilesRequested(string scorePath) NavigationControl { id: navCtrl @@ -67,12 +71,65 @@ FocusScope { enabled: root.enabled hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: function(mouse) { + navCtrl.requestActiveByInteraction() + + if (mouse.button === Qt.RightButton) { + if (contextMenuLoader.items.length > 0) { + contextMenuLoader.show(Qt.point(mouse.x, mouse.y)) + } + return + } - onClicked: { root.clicked() } } + ContextMenuLoader { + id: contextMenuLoader + + items: { + if (root.isCreateNew || root.isNoResultsFound) { + return [] + } + + let items = [ + { id: "open", title: qsTrc("project", "Open") } + ] + + if (root.isCloud) { + items.push({ id: "view-online", title: qsTrc("project", "View online") }) + } else { + items.push({ id: "reveal-in-file-browser", title: qsTrc("project", "Reveal in file browser") }) + } + + if (root.showRemoveFromRecentFiles) { + items.push({ id: "remove-from-recent-files", title: qsTrc("project", "Remove from recent files list") }) + } + + return items + } + + onHandleMenuItem: function(itemId) { + switch (itemId) { + case "open": + root.clicked() + break + case "view-online": + root.viewOnlineRequested(root.cloudScoreId) + break + case "reveal-in-file-browser": + root.revealInFileBrowserRequested(root.path) + break + case "remove-from-recent-files": + root.removeFromRecentFilesRequested(root.path) + break + } + } + } + Column { anchors.fill: parent diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreListItem.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreListItem.qml index bf9674d0e9722..a4e582690e605 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreListItem.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreListItem.qml @@ -38,6 +38,11 @@ ListItemBlank { property real itemInset: 12 property real columnSpacing: 44 property alias showBottomBorder: bottomBorder.visible + property bool showRemoveFromRecentFiles: false + + signal revealInFileBrowserRequested(string scorePath) + signal viewOnlineRequested(int scoreId) + signal removeFromRecentFilesRequested(string scorePath) implicitHeight: 64 @@ -50,6 +55,60 @@ ListItemBlank { focusBorder.anchors.bottomMargin: bottomBorder.visible ? bottomBorder.height : 0 + MouseArea { + anchors.fill: parent + enabled: root.visible && root.enabled + acceptedButtons: Qt.RightButton + onClicked: function(mouse) { + if (contextMenuLoader.items.length > 0) { + contextMenuLoader.show(Qt.point(mouse.x, mouse.y)) + } + } + } + + ContextMenuLoader { + id: contextMenuLoader + + items: { + if ((root.score.isCreateNew ?? false) || (root.score.isNoResultsFound ?? false)) { + return [] + } + + let items = [ + { id: "open", title: qsTrc("project", "Open") } + ] + + if (root.score.isCloud ?? false) { + items.push({ id: "view-online", title: qsTrc("project", "View online") }) + } else { + items.push({ id: "reveal-in-file-browser", title: qsTrc("project", "Reveal in file browser") }) + } + + if (root.showRemoveFromRecentFiles) { + items.push({ id: "remove-from-recent-files", title: qsTrc("project", "Remove from recent files list") }) + } + + return items + } + + onHandleMenuItem: function(itemId) { + switch (itemId) { + case "open": + root.clicked(null) + break + case "view-online": + root.viewOnlineRequested(root.score.scoreId ?? 0) + break + case "reveal-in-file-browser": + root.revealInFileBrowserRequested(root.score.path ?? "") + break + case "remove-from-recent-files": + root.removeFromRecentFilesRequested(root.score.path ?? "") + break + } + } + } + RowLayout { anchors.fill: parent anchors.leftMargin: root.itemInset diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresGridView.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresGridView.qml index 84c0dcbb1fffd..10ef1bdfcdaa7 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresGridView.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresGridView.qml @@ -33,6 +33,7 @@ Item { property string searchText property bool isNoResultsMessageAllowed: true + property bool allowRemoveFromRecentFiles: false property color backgroundColor: ui.theme.backgroundSecondaryColor property real sideMargin: 46 @@ -43,6 +44,9 @@ Item { signal createNewScoreRequested() signal openScoreRequested(var scorePath, var displayName) + signal revealInFileBrowserRequested(var scorePath) + signal viewOnlineRequested(var scoreId) + signal removeFromRecentFilesRequested(var scorePath) clip: true @@ -161,6 +165,7 @@ Item { isCloud: score.isCloud cloudScoreId: score.scoreId ?? 0 timeSinceModified: score.timeSinceModified ?? "" + showRemoveFromRecentFiles: root.allowRemoveFromRecentFiles onClicked: { if (isCreateNew) { @@ -169,6 +174,18 @@ Item { root.openScoreRequested(score.path, score.name) } } + + onRevealInFileBrowserRequested: function(scorePath) { + root.revealInFileBrowserRequested(scorePath) + } + + onViewOnlineRequested: function(scoreId) { + root.viewOnlineRequested(scoreId) + } + + onRemoveFromRecentFilesRequested: function(scorePath) { + root.removeFromRecentFilesRequested(scorePath) + } } } } diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresListView.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresListView.qml index 17d847d000e02..775c3b1d3742e 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresListView.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresListView.qml @@ -34,6 +34,7 @@ Item { property list columns property alias showNewScoreItem: newScoreItem.visible property string searchText + property bool allowRemoveFromRecentFiles: false property color backgroundColor: ui.theme.backgroundSecondaryColor property real sideMargin: 46 @@ -44,6 +45,9 @@ Item { signal createNewScoreRequested() signal openScoreRequested(var scorePath, var displayName) + signal revealInFileBrowserRequested(var scorePath) + signal viewOnlineRequested(var scoreId) + signal removeFromRecentFilesRequested(var scorePath) component ColumnItem : QtObject { property string header @@ -100,7 +104,8 @@ Item { navigation.column: 0 score: { - "name": qsTrc("project", "New score") + "name": qsTrc("project", "New score"), + "isCreateNew": true } thumbnailComponent: Rectangle { @@ -210,6 +215,7 @@ Item { itemInset: view.itemInset implicitHeight: view.rowHeight columnSpacing: view.columnSpacing + showRemoveFromRecentFiles: root.allowRemoveFromRecentFiles navigation.panel: navPanel navigation.row: index + 1 @@ -218,6 +224,18 @@ Item { onClicked: { root.openScoreRequested(score.path, score.name) } + + onRevealInFileBrowserRequested: function(scorePath) { + root.revealInFileBrowserRequested(scorePath) + } + + onViewOnlineRequested: function(scoreId) { + root.viewOnlineRequested(scoreId) + } + + onRemoveFromRecentFilesRequested: function(scorePath) { + root.removeFromRecentFilesRequested(scorePath) + } } } } diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresView.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresView.qml index 5e36714e09692..776dc52783b4e 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresView.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresView.qml @@ -40,4 +40,6 @@ Loader { signal createNewScoreRequested() signal openScoreRequested(var scorePath, var displayName) + signal revealInFileBrowserRequested(var scorePath) + signal viewOnlineRequested(var scoreId) } diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/recentscoresmodel.cpp b/src/project/qml/MuseScore/Project/internal/ScoresPage/recentscoresmodel.cpp index c9a44bb430f9d..a497191ab5721 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/recentscoresmodel.cpp +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/recentscoresmodel.cpp @@ -46,6 +46,11 @@ void RecentScoresModel::load() }); } +void RecentScoresModel::removeRecentScore(const QString& scorePath) +{ + recentFilesController()->removeRecentFile(scorePath); +} + void RecentScoresModel::setRecentScores(const std::vector& items) { if (m_items == items) { diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/recentscoresmodel.h b/src/project/qml/MuseScore/Project/internal/ScoresPage/recentscoresmodel.h index e4cc14167e18b..03388c296bb87 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/recentscoresmodel.h +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/recentscoresmodel.h @@ -46,6 +46,7 @@ class RecentScoresModel : public AbstractScoresModel, public muse::async::Asynca RecentScoresModel(QObject* parent = nullptr); void load() override; + Q_INVOKABLE void removeRecentScore(const QString& scorePath); QList nonScoreItemIndices() const override; diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/scorespagemodel.cpp b/src/project/qml/MuseScore/Project/internal/ScoresPage/scorespagemodel.cpp index 0a656a49286cb..8f0aea2a0f7e0 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/scorespagemodel.cpp +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/scorespagemodel.cpp @@ -25,6 +25,7 @@ #include #include "actions/actiontypes.h" +#include "log.h" using namespace mu::project; using namespace muse::actions; @@ -49,6 +50,35 @@ void ScoresPageModel::openScore(const QString& scorePath, const QString& display dispatcher()->dispatch("file-open", ActionData::make_arg2(QUrl::fromLocalFile(scorePath), displayNameOverride)); } +void ScoresPageModel::revealInFileBrowser(const QString& scorePath) +{ + muse::Ret ret = platformInteractive()->revealInFileBrowser(scorePath); + if (!ret) { + LOGE() << ret.toString(); + } +} + +void ScoresPageModel::viewOnline(int scoreId) +{ + if (scoreId <= 0) { + return; + } + + muse::RetVal scoreInfo = museScoreComService()->downloadScoreInfo(scoreId); + if (!scoreInfo.ret) { + LOGE() << scoreInfo.ret.toString(); + return; + } + + QUrl scoreUrl = QUrl::fromUserInput(scoreInfo.val.url); + if (!scoreUrl.isValid() || scoreUrl.isEmpty()) { + LOGE() << "Invalid score URL for cloud score" << scoreId << ":" << scoreInfo.val.url; + return; + } + + platformInteractive()->openUrl(scoreUrl); +} + void ScoresPageModel::openScoreManager() { platformInteractive()->openUrl(museScoreComService()->scoreManagerUrl()); diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/scorespagemodel.h b/src/project/qml/MuseScore/Project/internal/ScoresPage/scorespagemodel.h index c156960a2137c..621646ea3469a 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/scorespagemodel.h +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/scorespagemodel.h @@ -66,6 +66,8 @@ class ScoresPageModel : public QObject, public muse::Contextable Q_INVOKABLE void createNewScore(); Q_INVOKABLE void openOther(); Q_INVOKABLE void openScore(const QString& scorePath, const QString& displayNameOverride); + Q_INVOKABLE void revealInFileBrowser(const QString& scorePath); + Q_INVOKABLE void viewOnline(int scoreId); Q_INVOKABLE void openScoreManager(); signals: From 36bef2683ba3de2542d38debc2fc946462f68ade Mon Sep 17 00:00:00 2001 From: Casper Jeukendrup <48658420+cbjeukendrup@users.noreply.github.com> Date: Thu, 7 May 2026 21:33:54 +0200 Subject: [PATCH 2/4] Home > Scores: extract context menu into separate component --- .../qml/MuseScore/Project/CMakeLists.txt | 1 + .../internal/ScoresPage/ScoreGridItem.qml | 47 +++-------- .../ScoresPage/ScoreItemContextMenu.qml | 77 +++++++++++++++++++ .../internal/ScoresPage/ScoreListItem.qml | 47 +++-------- 4 files changed, 96 insertions(+), 76 deletions(-) create mode 100644 src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemContextMenu.qml diff --git a/src/project/qml/MuseScore/Project/CMakeLists.txt b/src/project/qml/MuseScore/Project/CMakeLists.txt index 2199e53819df9..cf18e767c4def 100644 --- a/src/project/qml/MuseScore/Project/CMakeLists.txt +++ b/src/project/qml/MuseScore/Project/CMakeLists.txt @@ -107,6 +107,7 @@ qt_add_qml_module(project_qml internal/ScoresPage/CloudScoresView.qml internal/ScoresPage/RecentScoresView.qml internal/ScoresPage/ScoreGridItem.qml + internal/ScoresPage/ScoreItemContextMenu.qml internal/ScoresPage/ScoreListItem.qml internal/ScoresPage/ScoresGridView.qml internal/ScoresPage/ScoresListView.qml diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreGridItem.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreGridItem.qml index 99c0b4a7d1f08..972e9d899c532 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreGridItem.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreGridItem.qml @@ -87,47 +87,18 @@ FocusScope { } } - ContextMenuLoader { + ScoreItemContextMenu { id: contextMenuLoader - items: { - if (root.isCreateNew || root.isNoResultsFound) { - return [] - } - - let items = [ - { id: "open", title: qsTrc("project", "Open") } - ] - - if (root.isCloud) { - items.push({ id: "view-online", title: qsTrc("project", "View online") }) - } else { - items.push({ id: "reveal-in-file-browser", title: qsTrc("project", "Reveal in file browser") }) - } - - if (root.showRemoveFromRecentFiles) { - items.push({ id: "remove-from-recent-files", title: qsTrc("project", "Remove from recent files list") }) - } - - return items - } + isCreateNew: root.isCreateNew + isNoResultsFound: root.isNoResultsFound + isCloud: root.isCloud + showRemoveFromRecentFiles: root.showRemoveFromRecentFiles - onHandleMenuItem: function(itemId) { - switch (itemId) { - case "open": - root.clicked() - break - case "view-online": - root.viewOnlineRequested(root.cloudScoreId) - break - case "reveal-in-file-browser": - root.revealInFileBrowserRequested(root.path) - break - case "remove-from-recent-files": - root.removeFromRecentFilesRequested(root.path) - break - } - } + onOpenRequested: root.clicked() + onViewOnlineRequested: root.viewOnlineRequested(root.cloudScoreId) + onRevealInFileBrowserRequested: root.revealInFileBrowserRequested(root.path) + onRemoveFromRecentFilesRequested: root.removeFromRecentFilesRequested(root.path) } Column { diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemContextMenu.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemContextMenu.qml new file mode 100644 index 0000000000000..f997e00d2445b --- /dev/null +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemContextMenu.qml @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import QtQuick + +import Muse.UiComponents + +ContextMenuLoader { + id: root + + property bool isCreateNew: false + property bool isNoResultsFound: false + property bool isCloud: false + property bool showRemoveFromRecentFiles: false + + signal openRequested() + signal revealInFileBrowserRequested() + signal viewOnlineRequested() + signal removeFromRecentFilesRequested() + + items: { + if (root.isCreateNew || root.isNoResultsFound) { + return [] + } + + let result = [ + { id: "open", title: qsTrc("project", "Open") } + ] + + if (root.isCloud) { + result.push({ id: "view-online", title: qsTrc("project", "View online") }) + } else { + result.push({ id: "reveal-in-file-browser", title: qsTrc("project", "Reveal in file browser") }) + } + + if (root.showRemoveFromRecentFiles) { + result.push({ id: "remove-from-recent-files", title: qsTrc("project", "Remove from recent files list") }) + } + + return result + } + + onHandleMenuItem: function(itemId) { + switch (itemId) { + case "open": + root.openRequested() + break + case "view-online": + root.viewOnlineRequested() + break + case "reveal-in-file-browser": + root.revealInFileBrowserRequested() + break + case "remove-from-recent-files": + root.removeFromRecentFilesRequested() + break + } + } +} diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreListItem.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreListItem.qml index a4e582690e605..1f743ca87ead9 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreListItem.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreListItem.qml @@ -66,47 +66,18 @@ ListItemBlank { } } - ContextMenuLoader { + ScoreItemContextMenu { id: contextMenuLoader - items: { - if ((root.score.isCreateNew ?? false) || (root.score.isNoResultsFound ?? false)) { - return [] - } - - let items = [ - { id: "open", title: qsTrc("project", "Open") } - ] - - if (root.score.isCloud ?? false) { - items.push({ id: "view-online", title: qsTrc("project", "View online") }) - } else { - items.push({ id: "reveal-in-file-browser", title: qsTrc("project", "Reveal in file browser") }) - } - - if (root.showRemoveFromRecentFiles) { - items.push({ id: "remove-from-recent-files", title: qsTrc("project", "Remove from recent files list") }) - } - - return items - } + isCreateNew: root.score.isCreateNew ?? false + isNoResultsFound: root.score.isNoResultsFound ?? false + isCloud: root.score.isCloud ?? false + showRemoveFromRecentFiles: root.showRemoveFromRecentFiles - onHandleMenuItem: function(itemId) { - switch (itemId) { - case "open": - root.clicked(null) - break - case "view-online": - root.viewOnlineRequested(root.score.scoreId ?? 0) - break - case "reveal-in-file-browser": - root.revealInFileBrowserRequested(root.score.path ?? "") - break - case "remove-from-recent-files": - root.removeFromRecentFilesRequested(root.score.path ?? "") - break - } - } + onOpenRequested: root.clicked(null) + onViewOnlineRequested: root.viewOnlineRequested(root.score.scoreId ?? 0) + onRevealInFileBrowserRequested: root.revealInFileBrowserRequested(root.score.path ?? "") + onRemoveFromRecentFilesRequested: root.removeFromRecentFilesRequested(root.score.path ?? "") } RowLayout { From ca480228f28c1e8de9bd2b8e009503425186dd4b Mon Sep 17 00:00:00 2001 From: Casper Jeukendrup <48658420+cbjeukendrup@users.noreply.github.com> Date: Thu, 7 May 2026 23:07:47 +0200 Subject: [PATCH 3/4] Home > Scores: expose context menu via keyboard-accessible button too --- muse | 2 +- .../qml/MuseScore/Project/CMakeLists.txt | 2 +- .../internal/ScoresPage/ScoreGridItem.qml | 63 +++++---- .../ScoresPage/ScoreItemContextMenu.qml | 77 ----------- .../ScoresPage/ScoreItemMenuButton.qml | 127 ++++++++++++++++++ .../internal/ScoresPage/ScoreListItem.qml | 46 ++++--- .../internal/ScoresPage/ScoresGridView.qml | 2 +- 7 files changed, 199 insertions(+), 120 deletions(-) delete mode 100644 src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemContextMenu.qml create mode 100644 src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemMenuButton.qml diff --git a/muse b/muse index 3e3d513e2161b..02949a9e51b5f 160000 --- a/muse +++ b/muse @@ -1 +1 @@ -Subproject commit 3e3d513e2161b4fa62bdc52800b7c49f21b33b93 +Subproject commit 02949a9e51b5f9607ea71e3b9ca6d9b8c592fb8f diff --git a/src/project/qml/MuseScore/Project/CMakeLists.txt b/src/project/qml/MuseScore/Project/CMakeLists.txt index cf18e767c4def..289f346a8b2d7 100644 --- a/src/project/qml/MuseScore/Project/CMakeLists.txt +++ b/src/project/qml/MuseScore/Project/CMakeLists.txt @@ -107,7 +107,7 @@ qt_add_qml_module(project_qml internal/ScoresPage/CloudScoresView.qml internal/ScoresPage/RecentScoresView.qml internal/ScoresPage/ScoreGridItem.qml - internal/ScoresPage/ScoreItemContextMenu.qml + internal/ScoresPage/ScoreItemMenuButton.qml internal/ScoresPage/ScoreListItem.qml internal/ScoresPage/ScoresGridView.qml internal/ScoresPage/ScoresListView.qml diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreGridItem.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreGridItem.qml index 972e9d899c532..c1bb0e48e2c05 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreGridItem.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreGridItem.qml @@ -66,7 +66,7 @@ FocusScope { } MouseArea { - id: mouseArea + id: rootMouseArea anchors.fill: parent enabled: root.enabled @@ -77,8 +77,8 @@ FocusScope { navCtrl.requestActiveByInteraction() if (mouse.button === Qt.RightButton) { - if (contextMenuLoader.items.length > 0) { - contextMenuLoader.show(Qt.point(mouse.x, mouse.y)) + if (contextMenu.menuModel.length > 0) { + contextMenu.show(Qt.point(mouse.x, mouse.y), root) } return } @@ -87,20 +87,6 @@ FocusScope { } } - ScoreItemContextMenu { - id: contextMenuLoader - - isCreateNew: root.isCreateNew - isNoResultsFound: root.isNoResultsFound - isCloud: root.isCloud - showRemoveFromRecentFiles: root.showRemoveFromRecentFiles - - onOpenRequested: root.clicked() - onViewOnlineRequested: root.viewOnlineRequested(root.cloudScoreId) - onRevealInFileBrowserRequested: root.revealInFileBrowserRequested(root.path) - onRemoveFromRecentFilesRequested: root.removeFromRecentFilesRequested(root.path) - } - Column { anchors.fill: parent @@ -126,7 +112,7 @@ FocusScope { sourceComponent: { if (root.isCreateNew) { - return addComp + return createNewComp } if (root.isNoResultsFound) { @@ -161,7 +147,7 @@ FocusScope { states: [ State { name: "NORMAL" - when: !mouseArea.containsMouse && !mouseArea.pressed + when: !rootMouseArea.containsMouse && !rootMouseArea.pressed PropertyChanges { target: thumbnail @@ -171,7 +157,7 @@ FocusScope { State { name: "HOVERED" - when: mouseArea.containsMouse && !mouseArea.pressed + when: rootMouseArea.containsMouse && !rootMouseArea.pressed PropertyChanges { target: thumbnail @@ -182,7 +168,7 @@ FocusScope { State { name: "PRESSED" - when: mouseArea.pressed + when: rootMouseArea.pressed PropertyChanges { target: thumbnail @@ -201,6 +187,35 @@ FocusScope { } } + ScoreItemMenuButton { + id: contextMenu + + anchors.top: parent.top + anchors.topMargin: 8 + anchors.right: parent.right + anchors.rightMargin: 8 + visible: menuModel.length > 0 + && (rootMouseArea.containsMouse + || mouseArea.containsMouse + || root.navigation.active + || navigation.active + || isMenuOpenedByButton) + + isCreateNew: root.isCreateNew + isNoResultsFound: root.isNoResultsFound + isCloud: root.isCloud + showRemoveFromRecentFiles: root.showRemoveFromRecentFiles + + navigation.panel: root.navigation.panel + navigation.row: root.navigation.row + navigation.column: root.navigation.column + 1 + + onOpenRequested: root.clicked() + onViewOnlineRequested: root.viewOnlineRequested(root.cloudScoreId) + onRevealInFileBrowserRequested: root.revealInFileBrowserRequested(root.path) + onRemoveFromRecentFilesRequested: root.removeFromRecentFilesRequested(root.path) + } + Loader { active: root.isCloud @@ -236,7 +251,7 @@ FocusScope { navigation.panel: root.navigation.panel navigation.row: root.navigation.row - navigation.column: root.navigation.column + 1 + navigation.column: root.navigation.column + 2 } CloudScoreIndicatorButton { @@ -247,7 +262,7 @@ FocusScope { navigation.panel: root.navigation.panel navigation.row: root.navigation.row - navigation.column: root.navigation.column + 2 + navigation.column: root.navigation.column + 3 onClicked: { if (isProgress) { @@ -292,7 +307,7 @@ FocusScope { } Component { - id: addComp + id: createNewComp Rectangle { anchors.fill: parent diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemContextMenu.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemContextMenu.qml deleted file mode 100644 index f997e00d2445b..0000000000000 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemContextMenu.qml +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-Studio-CLA-applies - * - * MuseScore Studio - * Music Composition & Notation - * - * Copyright (C) 2026 MuseScore Limited and others - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -import QtQuick - -import Muse.UiComponents - -ContextMenuLoader { - id: root - - property bool isCreateNew: false - property bool isNoResultsFound: false - property bool isCloud: false - property bool showRemoveFromRecentFiles: false - - signal openRequested() - signal revealInFileBrowserRequested() - signal viewOnlineRequested() - signal removeFromRecentFilesRequested() - - items: { - if (root.isCreateNew || root.isNoResultsFound) { - return [] - } - - let result = [ - { id: "open", title: qsTrc("project", "Open") } - ] - - if (root.isCloud) { - result.push({ id: "view-online", title: qsTrc("project", "View online") }) - } else { - result.push({ id: "reveal-in-file-browser", title: qsTrc("project", "Reveal in file browser") }) - } - - if (root.showRemoveFromRecentFiles) { - result.push({ id: "remove-from-recent-files", title: qsTrc("project", "Remove from recent files list") }) - } - - return result - } - - onHandleMenuItem: function(itemId) { - switch (itemId) { - case "open": - root.openRequested() - break - case "view-online": - root.viewOnlineRequested() - break - case "reveal-in-file-browser": - root.revealInFileBrowserRequested() - break - case "remove-from-recent-files": - root.removeFromRecentFilesRequested() - break - } - } -} diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemMenuButton.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemMenuButton.qml new file mode 100644 index 0000000000000..74298cc94c816 --- /dev/null +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemMenuButton.qml @@ -0,0 +1,127 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import QtQuick + +import Muse.Ui +import Muse.UiComponents + +FlatButton { + id: root + + property bool isCreateNew: false + property bool isNoResultsFound: false + property bool isCloud: false + property bool showRemoveFromRecentFiles: false + property alias isMenuOpened: menuLoader.isMenuOpened + property bool isMenuOpenedByButton: menuLoader.isMenuOpened && menuLoader.parent === root + property alias menuAnchorItem: menuLoader.menuAnchorItem + property alias parentWindow: menuLoader.parentWindow + property int menuAlign: 0 + + signal openRequested() + signal revealInFileBrowserRequested() + signal viewOnlineRequested() + signal removeFromRecentFilesRequested() + + function show(position, item) { + let target = item ? item : root + menuLoader.parent = target + + if (menuLoader.isMenuOpened) { + menuLoader.update(root.menuModel, position.x, position.y) + } else { + menuLoader.open(root.menuModel, position.x, position.y) + } + } + + function toggleMenu(item, x, y) { + menuLoader.parent = item ? item : root + menuLoader.toggleOpened(root.menuModel, x, y) + } + + function closeMenu() { + menuLoader.close() + } + + readonly property var menuModel: { + if (root.isCreateNew || root.isNoResultsFound) { + return [] + } + + let result = [ + { id: "open", title: qsTrc("project", "Open") } + ] + + if (root.isCloud) { + result.push({ id: "view-online", title: qsTrc("project", "View online") }) + } else { + result.push({ id: "reveal-in-file-browser", title: qsTrc("project", "Reveal in file browser") }) + } + + if (root.showRemoveFromRecentFiles) { + result.push({ id: "remove-from-recent-files", title: qsTrc("project", "Remove from recent files list") }) + } + + return result + } + + enabled: root.menuModel.length > 0 + + icon: IconCode.MENU_THREE_DOTS + transparent: !isMenuOpenedByButton + accentButton: isMenuOpenedByButton + width: 20 + height: 20 + + navigation.accessible.name: qsTrc("ui", "Menu") + + StyledMenuLoader { + id: menuLoader + + onHandleMenuItem: function(itemId) { + switch (itemId) { + case "open": + root.openRequested() + break + case "view-online": + root.viewOnlineRequested() + break + case "reveal-in-file-browser": + root.revealInFileBrowserRequested() + break + case "remove-from-recent-files": + root.removeFromRecentFilesRequested() + break + } + } + } + + onClicked: { + menuLoader.parent = root + + if (root.menuAlign !== 0) { + menuLoader.toggleOpenedWithAlign(root.menuModel, root.menuAlign) + } else { + menuLoader.toggleOpened(root.menuModel) + } + } +} diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreListItem.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreListItem.qml index 1f743ca87ead9..87864346a644e 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreListItem.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreListItem.qml @@ -60,26 +60,12 @@ ListItemBlank { enabled: root.visible && root.enabled acceptedButtons: Qt.RightButton onClicked: function(mouse) { - if (contextMenuLoader.items.length > 0) { - contextMenuLoader.show(Qt.point(mouse.x, mouse.y)) + if (contextMenu.menuModel.length > 0) { + contextMenu.show(Qt.point(mouse.x, mouse.y), root) } } } - ScoreItemContextMenu { - id: contextMenuLoader - - isCreateNew: root.score.isCreateNew ?? false - isNoResultsFound: root.score.isNoResultsFound ?? false - isCloud: root.score.isCloud ?? false - showRemoveFromRecentFiles: root.showRemoveFromRecentFiles - - onOpenRequested: root.clicked(null) - onViewOnlineRequested: root.viewOnlineRequested(root.score.scoreId ?? 0) - onRevealInFileBrowserRequested: root.revealInFileBrowserRequested(root.score.path ?? "") - onRemoveFromRecentFilesRequested: root.removeFromRecentFilesRequested(root.score.path ?? "") - } - RowLayout { anchors.fill: parent anchors.leftMargin: root.itemInset @@ -116,6 +102,34 @@ ListItemBlank { horizontalAlignment: Text.AlignLeft } + ScoreItemMenuButton { + id: contextMenu + + isCreateNew: root.score.isCreateNew ?? false + isNoResultsFound: root.score.isNoResultsFound ?? false + isCloud: root.score.isCloud ?? false + showRemoveFromRecentFiles: root.showRemoveFromRecentFiles + + Layout.alignment: Qt.AlignTrailing | Qt.AlignVCenter + Layout.preferredWidth: 20 + Layout.preferredHeight: 20 + visible: menuModel.length > 0 + && (root.mouseArea.containsMouse + || mouseArea.containsMouse + || root.navigation.active + || navigation.active + || isMenuOpenedByButton) + + navigation.panel: root.navigation.panel + navigation.row: root.navigation.row + navigation.column: 1 + + onOpenRequested: root.clicked(null) + onViewOnlineRequested: root.viewOnlineRequested(root.score.scoreId ?? 0) + onRevealInFileBrowserRequested: root.revealInFileBrowserRequested(root.score.path ?? "") + onRemoveFromRecentFilesRequested: root.removeFromRecentFilesRequested(root.score.path ?? "") + } + Loader { active: root.score.isCloud ?? false diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresGridView.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresGridView.qml index 10ef1bdfcdaa7..ccd1d3526038f 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresGridView.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresGridView.qml @@ -149,7 +149,7 @@ Item { navigation.panel: navPanel navigation.row: view.columns === 0 ? 0 : Math.floor(model.index / view.columns) - navigation.column: (model.index - (navigation.row * view.columns)) * 3 // * 3 because of controls inside ScoreItem + navigation.column: (model.index - (navigation.row * view.columns)) * 4 // * 4 because of controls inside ScoreItem navigation.onActiveChanged: { if (navigation.active) { view.positionViewAtIndex(index, GridView.Contain) From bba31ede86de02acc08e87c6a8952522ebdaa752 Mon Sep 17 00:00:00 2001 From: Casper Jeukendrup <48658420+cbjeukendrup@users.noreply.github.com> Date: Sun, 17 May 2026 16:44:11 +0200 Subject: [PATCH 4/4] Home > Scores: add separator to context menu --- .../Project/internal/ScoresPage/ScoreItemMenuButton.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemMenuButton.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemMenuButton.qml index 74298cc94c816..1407c80c862b9 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemMenuButton.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoreItemMenuButton.qml @@ -78,6 +78,7 @@ FlatButton { } if (root.showRemoveFromRecentFiles) { + result.push({}) // separator result.push({ id: "remove-from-recent-files", title: qsTrc("project", "Remove from recent files list") }) }