From 887b8ffaab268a9b19aa05fa5440fd66b3d4db37 Mon Sep 17 00:00:00 2001 From: Ayush Sah <75406648+xARSENICx@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:48:30 +0530 Subject: [PATCH 1/8] feat(qml): expose library menu actions --- src/qml/qmllibraryproxy.cpp | 174 +++++++++++++++++++++++++++++++++--- src/qml/qmllibraryproxy.h | 36 +++++++- 2 files changed, 196 insertions(+), 14 deletions(-) diff --git a/src/qml/qmllibraryproxy.cpp b/src/qml/qmllibraryproxy.cpp index 50bca25004f9..5bf6a2b9614d 100644 --- a/src/qml/qmllibraryproxy.cpp +++ b/src/qml/qmllibraryproxy.cpp @@ -2,13 +2,18 @@ #include #include +#include #include #include "control/controlobject.h" #include "library/library.h" +#include "library/library_prefs.h" #include "library/librarytablemodel.h" #include "library/trackcollection.h" #include "library/trackcollectionmanager.h" +#ifdef __ENGINEPRIME__ +#include "library/export/libraryexporter.h" +#endif #include "moc_qmllibraryproxy.cpp" #include "preferences/colorpalettesettings.h" #include "qml/qmlconfigproxy.h" @@ -101,16 +106,6 @@ mixxx::audio::FramePos getCurrentPlayPositionWithQuantize( } } // namespace -QmlLibraryProxy::QmlLibraryProxy( - std::shared_ptr pLibrary, QObject* parent) - : QObject(parent), - m_pLibrary(pLibrary), - m_pModelProperty(new QmlLibraryTrackListModel( - QList{}, m_pLibrary->trackTableModel(), this)), - m_pScanner(new QmlLibraryScannerProxy( - m_pLibrary->trackCollectionManager()->scanner(), this)) { -} - QmlLibraryScannerProxy::QmlLibraryScannerProxy(LibraryScanner* libraryScanner, QObject* parent) : QObject(parent), m_pLibraryScanner(libraryScanner), @@ -152,6 +147,98 @@ QmlLibraryScannerProxy::QmlLibraryScannerProxy(LibraryScanner* libraryScanner, Q }); } +QmlLibraryProxy::QmlLibraryProxy( + std::shared_ptr pLibrary, QObject* parent) + : QObject(parent), + m_pLibrary(pLibrary), + m_pModelProperty(new QmlLibraryTrackListModel( + QList{}, m_pLibrary->trackTableModel(), this)), + m_pScanner(new QmlLibraryScannerProxy( + m_pLibrary->trackCollectionManager()->scanner(), this)) { + connect(m_pScanner, + &QmlLibraryScannerProxy::stateChanged, + this, + &QmlLibraryProxy::libraryScanActiveChanged); + TrackCollectionManager* pTrackCollectionManager = + m_pLibrary->trackCollectionManager(); + VERIFY_OR_DEBUG_ASSERT(pTrackCollectionManager) { + return; + } + connect(pTrackCollectionManager, + &TrackCollectionManager::libraryScanSummary, + this, + [this](const LibraryScanResultSummary& result) { + const UserSettingsPointer pConfig = QmlConfigProxy::get(); + if (!pConfig || + !pConfig->getValue( + mixxx::library::prefs::kShowScanSummaryConfigKey, + true)) { + return; + } + if (result.autoscan && + result.numNewTracks == 0 && + result.numNewMissingTracks == 0 && + result.numRediscoveredTracks == 0) { + return; + } + + const QString title = tr("Library scan finished"); + if (result.noDirectoriesConfigured) { + emit libraryScanSummaryAvailable( + title, + tr("No music directories configured for scanning."), + tr("Add directories in the library preferences.")); + return; + } + + const QString text = tr("Scan took %1").arg(result.durationString); + QStringList details; + if (result.numNewTracks == 0 && + result.numMovedTracks == 0 && + result.numNewMissingTracks == 0 && + result.numRediscoveredTracks == 0) { + details.append(tr("No changes detected.")); + } else { + if (result.numNewTracks != 0) { + details.append(tr("%n new track(s) found", nullptr, result.numNewTracks)); + } + if (result.numMovedTracks != 0) { + details.append(tr("%n moved track(s) detected", + nullptr, + result.numMovedTracks)); + } + if (result.numNewMissingTracks != 0) { + details.append(tr("%n track(s) missing (%1 total)", + nullptr, + result.numNewMissingTracks) + .arg(result.numMissingTracks)); + } + if (result.numRediscoveredTracks != 0) { + details.append(tr("%n track(s) rediscovered", + nullptr, + result.numRediscoveredTracks)); + } + } + details.append(tr("%n track(s) in total", nullptr, result.tracksTotal)); + emit libraryScanSummaryAvailable(title, text, details.join(QLatin1Char('\n'))); + }); +#ifdef __ENGINEPRIME__ + m_pLibraryExporter = m_pLibrary->makeLibraryExporter(nullptr); + connect(m_pLibrary.get(), + &Library::exportLibrary, + m_pLibraryExporter.get(), + &mixxx::LibraryExporter::slotRequestExport); + connect(m_pLibrary.get(), + &Library::exportCrate, + m_pLibraryExporter.get(), + &mixxx::LibraryExporter::slotRequestExportWithInitialCrate); + connect(m_pLibrary.get(), + &Library::exportPlaylist, + m_pLibraryExporter.get(), + &mixxx::LibraryExporter::slotRequestExportWithInitialPlaylist); +#endif +} + QmlLibraryProxy::~QmlLibraryProxy() = default; QmlLibraryTrackListModel* QmlLibraryProxy::model() const { @@ -166,6 +253,73 @@ void QmlLibraryProxy::analyze(const QmlTrackProxy* track) const { emit s_pLibrary->analyzeTracks({track->internal()->getId()}); } +void QmlLibraryProxy::createCrate() { + VERIFY_OR_DEBUG_ASSERT(s_pLibrary) { + return; + } + s_pLibrary->slotCreateCrate(); +} + +void QmlLibraryProxy::createPlaylist() { + VERIFY_OR_DEBUG_ASSERT(s_pLibrary) { + return; + } + s_pLibrary->slotCreatePlaylist(); +} + +bool QmlLibraryProxy::enginePrimeExportAvailable() const { +#ifdef __ENGINEPRIME__ + return true; +#else + return false; +#endif +} + +void QmlLibraryProxy::exportLibrary() { +#ifdef __ENGINEPRIME__ + VERIFY_OR_DEBUG_ASSERT(s_pLibrary) { + return; + } + emit s_pLibrary->exportLibrary(); +#endif +} + +void QmlLibraryProxy::rescanLibrary() { + VERIFY_OR_DEBUG_ASSERT(s_pLibrary) { + return; + } + if (libraryScanActive()) { + return; + } + TrackCollectionManager* pTrackCollectionManager = + s_pLibrary->trackCollectionManager(); + VERIFY_OR_DEBUG_ASSERT(pTrackCollectionManager) { + return; + } + pTrackCollectionManager->startLibraryScan(); +} + +void QmlLibraryProxy::searchInCurrentView() { + VERIFY_OR_DEBUG_ASSERT(s_pLibrary) { + return; + } + s_pLibrary->slotSearchInCurrentView(); +} + +void QmlLibraryProxy::searchInTracksLibrary() { + VERIFY_OR_DEBUG_ASSERT(s_pLibrary) { + return; + } + s_pLibrary->slotSearchInAllTracks(); +} + +void QmlLibraryProxy::showAutoDJ() { + VERIFY_OR_DEBUG_ASSERT(s_pLibrary) { + return; + } + s_pLibrary->showAutoDJ(); +} + QString QmlLibraryProxy::deckHotcueLabel( QmlTrackProxy* track, int hotcueNumber) const { diff --git a/src/qml/qmllibraryproxy.h b/src/qml/qmllibraryproxy.h index 996d29564fdb..c18307446892 100644 --- a/src/qml/qmllibraryproxy.h +++ b/src/qml/qmllibraryproxy.h @@ -18,6 +18,7 @@ class LibraryScanner; class KeyboardEventFilter; namespace mixxx { +class LibraryExporter; namespace qml { class QmlLibrarySource : public QObject { @@ -87,8 +88,10 @@ class QmlLibraryScannerProxy : public QObject { class QmlLibraryProxy : public QObject { Q_OBJECT Q_PROPERTY(mixxx::qml::QmlLibraryTrackListModel* model MEMBER m_pModelProperty CONSTANT) - Q_PROPERTY(QQmlListProperty sources READ sources CONSTANT) + Q_PROPERTY(QQmlListProperty sources READ sources CONSTANT) Q_PROPERTY(mixxx::qml::QmlLibraryScannerProxy* scanner MEMBER m_pScanner CONSTANT) + Q_PROPERTY(bool libraryScanActive READ libraryScanActive NOTIFY libraryScanActiveChanged) + Q_PROPERTY(bool enginePrimeExportAvailable READ enginePrimeExportAvailable CONSTANT) QML_NAMED_ELEMENT(Library) QML_SINGLETON @@ -142,9 +145,12 @@ class QmlLibraryProxy : public QObject { &QmlLibraryProxy::sources_clear}; } - Q_INVOKABLE AddResult addSource(const QUrl& newPath); - Q_INVOKABLE RemoveResult removeSource(const QUrl& oldPath, SourceRemovalType type); - Q_INVOKABLE RelocateResult relinkSource(const QUrl& oldPath, const QUrl& newPath); + Q_INVOKABLE mixxx::qml::QmlLibraryProxy::AddResult addSource(const QUrl& newPath); + Q_INVOKABLE mixxx::qml::QmlLibraryProxy::RemoveResult removeSource( + const QUrl& oldPath, + mixxx::qml::QmlLibraryProxy::SourceRemovalType type); + Q_INVOKABLE mixxx::qml::QmlLibraryProxy::RelocateResult relinkSource( + const QUrl& oldPath, const QUrl& newPath); static void registerKeyboardEventFilter(std::shared_ptr pKeyboard) { s_pKeyboard = std::move(pKeyboard); @@ -155,7 +161,19 @@ class QmlLibraryProxy : public QObject { } QmlLibraryTrackListModel* model() const; + bool libraryScanActive() const { + return m_pScanner && m_pScanner->isRunning(); + } + bool enginePrimeExportAvailable() const; + Q_INVOKABLE void analyze(const mixxx::qml::QmlTrackProxy* track) const; + Q_INVOKABLE void createCrate(); + Q_INVOKABLE void createPlaylist(); + Q_INVOKABLE void exportLibrary(); + Q_INVOKABLE void rescanLibrary(); + Q_INVOKABLE void searchInCurrentView(); + Q_INVOKABLE void searchInTracksLibrary(); + Q_INVOKABLE void showAutoDJ(); Q_INVOKABLE QString deckHotcueLabel( mixxx::qml::QmlTrackProxy* track, int hotcueNumber) const; @@ -172,6 +190,13 @@ class QmlLibraryProxy : public QObject { mixxx::qml::QmlTrackProxy* track, int hotcueNumber); + signals: + void libraryScanActiveChanged(); + void libraryScanSummaryAvailable( + const QString& title, + const QString& text, + const QString& informativeText); + private: static inline std::shared_ptr s_pLibrary; @@ -186,6 +211,9 @@ class QmlLibraryProxy : public QObject { QQmlListProperty* property, qsizetype index); static void sources_clear(QQmlListProperty* property); static inline std::shared_ptr s_pKeyboard; +#ifdef __ENGINEPRIME__ + std::unique_ptr m_pLibraryExporter; +#endif }; } // namespace qml From 6b97aca4229cc4ba8bbc64121509b4a01e25e28d Mon Sep 17 00:00:00 2001 From: Ayush Sah <75406648+xARSENICx@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:48:44 +0530 Subject: [PATCH 2/8] feat(qml): expose player menu helpers --- src/qml/qmlplayermanagerproxy.cpp | 33 ++++++++++++++++++++++++++++++- src/qml/qmlplayermanagerproxy.h | 8 ++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/qml/qmlplayermanagerproxy.cpp b/src/qml/qmlplayermanagerproxy.cpp index 82b6dcf5f7ff..c86d01176a30 100644 --- a/src/qml/qmlplayermanagerproxy.cpp +++ b/src/qml/qmlplayermanagerproxy.cpp @@ -2,17 +2,23 @@ #include +#include "library/library_prefs.h" #include "mixer/playermanager.h" #include "moc_qmlplayermanagerproxy.cpp" +#include "qml/qmlconfigproxy.h" #include "qml/qmlplayerproxy.h" +#include "sources/soundsourceproxy.h" #include "track/track_decl.h" +#include "util/fileinfo.h" +#include "util/sandbox.h" namespace mixxx { namespace qml { QmlPlayerManagerProxy::QmlPlayerManagerProxy( std::shared_ptr pPlayerManager, QObject* parent) - : QObject(parent), m_pPlayerManager(pPlayerManager) { + : QObject(parent), + m_pPlayerManager(pPlayerManager) { } QmlPlayerProxy* QmlPlayerManagerProxy::getPlayer(const QString& group) { @@ -69,6 +75,18 @@ void QmlPlayerManagerProxy::loadLocationUrlIntoNextAvailableDeck( } } +void QmlPlayerManagerProxy::loadLocationUrlToDeck( + const QUrl& locationUrl, int deck) { + if (!locationUrl.isLocalFile()) { + qWarning() << "QmlPlayerManagerProxy: URL" << locationUrl + << "is not a local file!"; + return; + } + mixxx::FileInfo fileInfo(locationUrl.toLocalFile()); + Sandbox::createSecurityToken(&fileInfo); + m_pPlayerManager->slotLoadToDeck(fileInfo.location(), deck); +} + void QmlPlayerManagerProxy::loadLocationToPlayer( const QString& location, const QString& group, bool play) { m_pPlayerManager->slotLoadLocationToPlayer(location, group, play); @@ -95,6 +113,19 @@ void QmlPlayerManagerProxy::showNoVinylControlInputConfiguredWarning() { emit m_pPlayerManager->noVinylControlInputConfigured(); } +QStringList QmlPlayerManagerProxy::supportedAudioFileNameFilters() const { + return {tr("Audio (%1)") + .arg(SoundSourceProxy::getSupportedFileNamePatterns().join(QLatin1Char(' ')))}; +} + +QUrl QmlPlayerManagerProxy::initialTrackDirectoryUrl() const { + const UserSettingsPointer pConfig = QmlConfigProxy::get(); + return pConfig + ? QUrl::fromLocalFile(pConfig->getValueString( + mixxx::library::prefs::kLegacyDirectoryConfigKey)) + : QUrl(); +} + // static QmlPlayerManagerProxy* QmlPlayerManagerProxy::create(QQmlEngine* pQmlEngine, QJSEngine* pJsEngine) { // The implementation of this method is mostly taken from the code example diff --git a/src/qml/qmlplayermanagerproxy.h b/src/qml/qmlplayermanagerproxy.h index ba3b1a2d4f82..0a9129279134 100644 --- a/src/qml/qmlplayermanagerproxy.h +++ b/src/qml/qmlplayermanagerproxy.h @@ -2,6 +2,7 @@ #include #include #include +#include #include "mixer/playermanager.h" #include "qml/qmlplayerproxy.h" @@ -11,6 +12,9 @@ namespace qml { class QmlPlayerManagerProxy : public QObject { Q_OBJECT + Q_PROPERTY(QStringList supportedAudioFileNameFilters READ + supportedAudioFileNameFilters CONSTANT) + Q_PROPERTY(QUrl initialTrackDirectoryUrl READ initialTrackDirectoryUrl CONSTANT) QML_NAMED_ELEMENT(PlayerManager) QML_SINGLETON public: @@ -22,6 +26,7 @@ class QmlPlayerManagerProxy : public QObject { Q_INVOKABLE void loadLocationIntoNextAvailableDeck(const QString& location, bool play = false); Q_INVOKABLE void loadLocationUrlIntoNextAvailableDeck( const QUrl& locationUrl, bool play = false); + Q_INVOKABLE void loadLocationUrlToDeck(const QUrl& locationUrl, int deck); Q_INVOKABLE void loadLocationToPlayer( const QString& location, const QString& group, bool play = false); Q_INVOKABLE void loadTrackToPlayer(TrackPointer track, @@ -33,6 +38,9 @@ class QmlPlayerManagerProxy : public QObject { Q_INVOKABLE void showNoDeckPassthroughInputConfiguredWarning(); Q_INVOKABLE void showNoVinylControlInputConfiguredWarning(); + QStringList supportedAudioFileNameFilters() const; + QUrl initialTrackDirectoryUrl() const; + static QmlPlayerManagerProxy* create(QQmlEngine* pQmlEngine, QJSEngine* pJsEngine); static void registerPlayerManager(std::shared_ptr pPlayerManager) { s_pPlayerManager = std::move(pPlayerManager); From dfbc0187cbb73fc080fa3ca4e5212cf9a8df2bff Mon Sep 17 00:00:00 2001 From: Ayush Sah <75406648+xARSENICx@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:48:58 +0530 Subject: [PATCH 3/8] feat(qml): add application menu proxy --- CMakeLists.txt | 1 + src/coreservices.cpp | 5 ++ src/qml/qmlapplication.cpp | 20 ++--- src/qml/qmlapplication.h | 2 - src/qml/qmlapplicationproxy.cpp | 127 ++++++++++++++++++++++++++++++++ src/qml/qmlapplicationproxy.h | 78 ++++++++++++++++++++ 6 files changed, 217 insertions(+), 16 deletions(-) create mode 100644 src/qml/qmlapplicationproxy.cpp create mode 100644 src/qml/qmlapplicationproxy.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d81e2ff824d..a9122ef381e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3983,6 +3983,7 @@ if(QML) PRIVATE src/qml/asyncimageprovider.cpp src/qml/qmlapplication.cpp + src/qml/qmlapplicationproxy.cpp src/qml/qmlautoreload.cpp src/qml/qmlbatteryproxy.cpp src/qml/qmlbeatsmodel.cpp diff --git a/src/coreservices.cpp b/src/coreservices.cpp index fd6aaa609e66..2a593a66904b 100644 --- a/src/coreservices.cpp +++ b/src/coreservices.cpp @@ -42,6 +42,7 @@ #include #include +#include "qml/qmlapplicationproxy.h" #include "qml/qmlconfigproxy.h" #include "qml/qmleffectsmanagerproxy.h" #include "qml/qmllibraryproxy.h" @@ -809,6 +810,8 @@ void CoreServices::initializeQMLSingletons() { // singletons to that they can be accessed by components instantiated by // QML, which would also be suboptimal. mixxx::qml::QmlEffectsManagerProxy::registerEffectsManager(getEffectsManager()); + mixxx::qml::QmlApplicationProxy::registerUserSettings(getSettings()); + mixxx::qml::QmlApplicationProxy::registerKeyboardEventFilter(getKeyboardEventFilter()); mixxx::qml::QmlPlayerManagerProxy::registerPlayerManager(getPlayerManager()); mixxx::qml::QmlConfigProxy::registerUserSettings(getSettings()); mixxx::qml::QmlLibraryProxy::registerLibrary(getLibrary()); @@ -906,6 +909,8 @@ void CoreServices::finalize() { #ifdef MIXXX_USE_QML // Delete all the QML singletons in order to prevent controller leaks mixxx::qml::QmlEffectsManagerProxy::registerEffectsManager(nullptr); + mixxx::qml::QmlApplicationProxy::registerUserSettings(nullptr); + mixxx::qml::QmlApplicationProxy::registerKeyboardEventFilter(nullptr); mixxx::qml::QmlPlayerManagerProxy::registerPlayerManager(nullptr); mixxx::qml::QmlConfigProxy::registerUserSettings(nullptr); mixxx::qml::QmlLibraryProxy::registerLibrary(nullptr); diff --git a/src/qml/qmlapplication.cpp b/src/qml/qmlapplication.cpp index b91141c364ba..d06870784e2a 100644 --- a/src/qml/qmlapplication.cpp +++ b/src/qml/qmlapplication.cpp @@ -1,10 +1,6 @@ #include "qmlapplication.h" -#include #include -#include -#include -#include #include #include #include @@ -19,6 +15,7 @@ #include "moc_qmlapplication.cpp" #include "preferences/configobject.h" #include "qml/asyncimageprovider.h" +#include "qml/qmlapplicationproxy.h" #include "qml/qmldlgpreferencesproxy.h" #include "qml/qmlrecordingproxy.h" #include "soundio/soundmanager.h" @@ -180,16 +177,10 @@ QmlApplication::QmlApplication( QmlDlgPreferencesProxy::s_pInstance = std::make_unique(pDlgPreferences, this); QmlRecordingProxy::s_pRecordingManager = m_pCoreServices->getRecordingManager(); - - m_pMenuBar = std::make_unique(); - QMenu* pApplicationMenu = m_pMenuBar->addMenu(QCoreApplication::applicationName()); - QAction* pPreferencesAction = pApplicationMenu->addAction(tr("&Preferences")); - pPreferencesAction->setMenuRole(QAction::PreferencesRole); - pPreferencesAction->setShortcut(QKeySequence::Preferences); - connect(pPreferencesAction, &QAction::triggered, this, [pDlgPreferences]() { - pDlgPreferences->show(); - pDlgPreferences->raise(); - pDlgPreferences->activateWindow(); + QmlApplicationProxy::registerReloadCallback([this]() { + QTimer::singleShot(0, this, [this]() { + loadQml(m_mainFilePath); + }); }); const QStringList visualGroups = @@ -267,6 +258,7 @@ void QmlApplication::slotFrameSwapped() { } QmlApplication::~QmlApplication() { + QmlApplicationProxy::registerReloadCallback({}); // Delete all the QML singletons in order to prevent leak detection in CoreService QmlRecordingProxy::s_pRecordingManager.reset(); QmlDlgPreferencesProxy::s_pInstance.reset(); diff --git a/src/qml/qmlapplication.h b/src/qml/qmlapplication.h index 0bca105598aa..0c167a43e4b4 100644 --- a/src/qml/qmlapplication.h +++ b/src/qml/qmlapplication.h @@ -13,7 +13,6 @@ class ControlProxy; class ControlPushButton; class GuiTick; -class QMenuBar; class VisualsManager; #if defined(Q_OS_ANDROID) class QQuickWindow; @@ -64,7 +63,6 @@ class QmlApplication : public QObject { QString m_mainFilePath; std::unique_ptr m_pAppEngine; - std::unique_ptr m_pMenuBar; bool m_loadSucceeded; QmlAutoReload m_autoReload; diff --git a/src/qml/qmlapplicationproxy.cpp b/src/qml/qmlapplicationproxy.cpp new file mode 100644 index 000000000000..8b67c079de54 --- /dev/null +++ b/src/qml/qmlapplicationproxy.cpp @@ -0,0 +1,127 @@ +#include "qml/qmlapplicationproxy.h" + +#include "controllers/keyboard/keyboardeventfilter.h" +#include "moc_qmlapplicationproxy.cpp" +#include "preferences/configobject.h" +#include "util/cmdlineargs.h" +#include "util/experiment.h" +#include "util/versionstore.h" + +namespace { +const ConfigKey kDebuggerEnabledConfigKey( + QStringLiteral("[ScriptDebugger]"), QStringLiteral("Enabled")); +} // namespace + +namespace mixxx { +namespace qml { + +QmlApplicationProxy::QmlApplicationProxy(QObject* pParent) + : QObject(pParent) { + VERIFY_OR_DEBUG_ASSERT(s_pKeyboardEventFilter) { + return; + } + connect(s_pKeyboardEventFilter.get(), + &KeyboardEventFilter::shortcutsEnabled, + this, + &QmlApplicationProxy::keyboardShortcutsEnabledChanged); +} + +bool QmlApplicationProxy::keyboardShortcutsEnabled() const { + return s_pKeyboardEventFilter && s_pKeyboardEventFilter->isEnabled(); +} + +void QmlApplicationProxy::setKeyboardShortcutsEnabled(bool enabled) { + VERIFY_OR_DEBUG_ASSERT(s_pKeyboardEventFilter) { + return; + } + if (s_pKeyboardEventFilter->isEnabled() == enabled) { + return; + } + s_pKeyboardEventFilter->setEnabled(enabled); +} + +bool QmlApplicationProxy::developerMode() const { + return CmdlineArgs::Instance().getDeveloper(); +} + +bool QmlApplicationProxy::experimentStatsEnabled() const { + return Experiment::isExperiment(); +} + +bool QmlApplicationProxy::baseStatsEnabled() const { + return Experiment::isBase(); +} + +bool QmlApplicationProxy::debuggerEnabled() const { + return s_pConfig && s_pConfig->getValue(kDebuggerEnabledConfigKey, false); +} + +void QmlApplicationProxy::setDebuggerEnabled(bool enabled) { + VERIFY_OR_DEBUG_ASSERT(s_pConfig) { + return; + } + if (debuggerEnabled() == enabled) { + return; + } + s_pConfig->setValue(kDebuggerEnabledConfigKey, enabled); + emit debuggerEnabledChanged(); +} + +QUrl QmlApplicationProxy::settingsDirectoryUrl() const { + return s_pConfig ? QUrl::fromLocalFile(s_pConfig->getSettingsPath()) : QUrl(); +} + +QString QmlApplicationProxy::applicationName() const { + return VersionStore::applicationName(); +} + +QString QmlApplicationProxy::version() const { + return VersionStore::version(); +} + +QString QmlApplicationProxy::platform() const { + return VersionStore::platform(); +} + +void QmlApplicationProxy::setExperimentStatsEnabled(bool enabled) { + if (enabled) { + Experiment::setExperiment(); + } else { + Experiment::disable(); + } + emit statsModeChanged(); +} + +void QmlApplicationProxy::setBaseStatsEnabled(bool enabled) { + if (enabled) { + Experiment::setBase(); + } else { + Experiment::disable(); + } + emit statsModeChanged(); +} + +QString QmlApplicationProxy::menuShortcut( + const QString& command, const QString& defaultShortcut) const { + VERIFY_OR_DEBUG_ASSERT(s_pKeyboardEventFilter) { + return defaultShortcut; + } + return s_pKeyboardEventFilter->getKeyboardConfig()->getValue( + ConfigKey(QStringLiteral("[KeyboardShortcuts]"), command), + defaultShortcut); +} + +void QmlApplicationProxy::reloadSkin() { + if (s_reloadCallback) { + s_reloadCallback(); + } +} + +QmlApplicationProxy* QmlApplicationProxy::create( + QQmlEngine* pQmlEngine, QJSEngine* pJsEngine) { + Q_UNUSED(pJsEngine); + return new QmlApplicationProxy(pQmlEngine); +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlapplicationproxy.h b/src/qml/qmlapplicationproxy.h new file mode 100644 index 000000000000..7e6b20ddd9c2 --- /dev/null +++ b/src/qml/qmlapplicationproxy.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "preferences/usersettings.h" + +class KeyboardEventFilter; + +namespace mixxx { +namespace qml { + +class QmlApplicationProxy : public QObject { + Q_OBJECT + Q_PROPERTY(bool keyboardShortcutsEnabled READ keyboardShortcutsEnabled WRITE + setKeyboardShortcutsEnabled NOTIFY keyboardShortcutsEnabledChanged) + Q_PROPERTY(bool developerMode READ developerMode CONSTANT) + Q_PROPERTY(bool experimentStatsEnabled READ experimentStatsEnabled NOTIFY statsModeChanged) + Q_PROPERTY(bool baseStatsEnabled READ baseStatsEnabled NOTIFY statsModeChanged) + Q_PROPERTY(bool debuggerEnabled READ debuggerEnabled WRITE setDebuggerEnabled NOTIFY + debuggerEnabledChanged) + Q_PROPERTY(QUrl settingsDirectoryUrl READ settingsDirectoryUrl CONSTANT) + Q_PROPERTY(QString applicationName READ applicationName CONSTANT) + Q_PROPERTY(QString version READ version CONSTANT) + Q_PROPERTY(QString platform READ platform CONSTANT) + QML_NAMED_ELEMENT(Application) + QML_SINGLETON + + public: + explicit QmlApplicationProxy(QObject* pParent = nullptr); + + bool keyboardShortcutsEnabled() const; + void setKeyboardShortcutsEnabled(bool enabled); + bool developerMode() const; + bool experimentStatsEnabled() const; + bool baseStatsEnabled() const; + bool debuggerEnabled() const; + void setDebuggerEnabled(bool enabled); + QUrl settingsDirectoryUrl() const; + QString applicationName() const; + QString version() const; + QString platform() const; + + Q_INVOKABLE void setExperimentStatsEnabled(bool enabled); + Q_INVOKABLE void setBaseStatsEnabled(bool enabled); + Q_INVOKABLE QString menuShortcut( + const QString& command, const QString& defaultShortcut) const; + Q_INVOKABLE void reloadSkin(); + + static QmlApplicationProxy* create(QQmlEngine* pQmlEngine, QJSEngine* pJsEngine); + static void registerUserSettings(UserSettingsPointer pConfig) { + s_pConfig = std::move(pConfig); + } + static void registerKeyboardEventFilter( + std::shared_ptr pKeyboardEventFilter) { + s_pKeyboardEventFilter = std::move(pKeyboardEventFilter); + } + static void registerReloadCallback(std::function reloadCallback) { + s_reloadCallback = std::move(reloadCallback); + } + + signals: + void keyboardShortcutsEnabledChanged(); + void statsModeChanged(); + void debuggerEnabledChanged(); + + private: + static inline UserSettingsPointer s_pConfig; + static inline std::shared_ptr s_pKeyboardEventFilter; + static inline std::function s_reloadCallback; +}; + +} // namespace qml +} // namespace mixxx From 171a15a20cfb73961c08d817e42ef6468c40c86f Mon Sep 17 00:00:00 2001 From: Ayush Sah <75406648+xARSENICx@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:49:25 +0530 Subject: [PATCH 4/8] feat(qml): add native application menu --- res/qml/ApplicationMenuCommands.qml | 107 +++++++ res/qml/LibraryScanSummaryDialog.qml | 20 ++ res/qml/MainMenuBar.qml | 398 +++++++++++++++++++++++++++ res/qml/main.qml | 25 ++ 4 files changed, 550 insertions(+) create mode 100644 res/qml/ApplicationMenuCommands.qml create mode 100644 res/qml/LibraryScanSummaryDialog.qml create mode 100644 res/qml/MainMenuBar.qml diff --git a/res/qml/ApplicationMenuCommands.qml b/res/qml/ApplicationMenuCommands.qml new file mode 100644 index 000000000000..b4e879ab21bc --- /dev/null +++ b/res/qml/ApplicationMenuCommands.qml @@ -0,0 +1,107 @@ +import Mixxx 1.0 as Mixxx +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs +import QtQuick.Window + +Item { + id: root + + required property ApplicationWindow applicationWindow + readonly property bool fullScreen: applicationWindow.visibility === Window.FullScreen + property int pendingDeck: 1 + + signal showDeveloperToolsRequested + + function loadTrackToDeck(deck) { + pendingDeck = deck; + const playControl = [deck1PlayControl, deck2PlayControl, deck3PlayControl, deck4PlayControl][deck - 1]; + if (playControl && playControl.value > 0) { + confirmLoadDialog.text = qsTr("Deck %1 is currently playing a track.\nAre you sure you want to load a new track?").arg(deck); + confirmLoadDialog.open(); + } else { + openTrackFileDialog(); + } + } + function openTrackFileDialog() { + trackFileDialog.title = qsTr("Load track to Deck %1").arg(pendingDeck); + trackFileDialog.currentFolder = Mixxx.PlayerManager.initialTrackDirectoryUrl; + Qt.callLater(function() { + trackFileDialog.open(); + }); + } + function toggleFullScreen() { + applicationWindow.visibility = applicationWindow.visibility === Window.FullScreen ? Window.Windowed : Window.FullScreen; + } + function showAbout() { + aboutDialog.open(); + } + function showKeywheel() { + keywheelDialog.open(); + } + + FileDialog { + id: trackFileDialog + + fileMode: FileDialog.OpenFile + nameFilters: Mixxx.PlayerManager.supportedAudioFileNameFilters + + onAccepted: Mixxx.PlayerManager.loadLocationUrlToDeck(selectedFile, root.pendingDeck) + } + MessageDialog { + id: confirmLoadDialog + + buttons: MessageDialog.Yes | MessageDialog.No + title: Mixxx.Application.applicationName + + onAccepted: root.openTrackFileDialog() + } + MessageDialog { + id: aboutDialog + + buttons: MessageDialog.Ok + informativeText: qsTr("%1\n%2").arg(Mixxx.Application.platform).arg("https://mixxx.org") + text: qsTr("%1 %2").arg(Mixxx.Application.applicationName).arg(Mixxx.Application.version) + title: qsTr("About %1").arg(Mixxx.Application.applicationName) + } + Dialog { + id: keywheelDialog + + height: Math.min(620, root.applicationWindow.height - 80) + modal: false + standardButtons: Dialog.Close + title: qsTr("Keywheel") + width: height + x: Math.round((root.applicationWindow.width - width) / 2) + y: Math.round((root.applicationWindow.height - height) / 2) + + contentItem: Image { + fillMode: Image.PreserveAspectFit + source: "../images/keywheel/keywheel.svg" + } + } + Mixxx.ControlProxy { + id: deck1PlayControl + + group: "[Channel1]" + key: "play" + } + Mixxx.ControlProxy { + id: deck2PlayControl + + group: "[Channel2]" + key: "play" + } + Mixxx.ControlProxy { + id: deck3PlayControl + + group: "[Channel3]" + key: "play" + } + Mixxx.ControlProxy { + id: deck4PlayControl + + group: "[Channel4]" + key: "play" + } +} diff --git a/res/qml/LibraryScanSummaryDialog.qml b/res/qml/LibraryScanSummaryDialog.qml new file mode 100644 index 000000000000..883d2e92e504 --- /dev/null +++ b/res/qml/LibraryScanSummaryDialog.qml @@ -0,0 +1,20 @@ +import Mixxx 1.0 as Mixxx +import QtQuick +import QtQuick.Dialogs + +MessageDialog { + id: root + + buttons: MessageDialog.Ok + + Connections { + function onLibraryScanSummaryAvailable(title, text, informativeText) { + root.title = title; + root.text = text; + root.informativeText = informativeText; + root.open(); + } + + target: Mixxx.Library + } +} diff --git a/res/qml/MainMenuBar.qml b/res/qml/MainMenuBar.qml new file mode 100644 index 000000000000..a026965849f3 --- /dev/null +++ b/res/qml/MainMenuBar.qml @@ -0,0 +1,398 @@ +pragma ComponentBehavior: Bound + +import Mixxx 1.0 as Mixxx +import QtQuick +import QtQuick.Controls +import QtQuick.Window + +MenuBar { + id: root + + required property ApplicationWindow applicationWindow + required property var commands + property int numberOfDecks: 4 + + signal focusLibrarySearchRequested + + visible: Qt.platform.os === "osx" + + Menu { + title: qsTr("&File") + + Action { + shortcut: Mixxx.Application.menuShortcut("FileMenu_LoadDeck1", "Ctrl+O") + text: qsTr("Load Track to Deck &1") + + onTriggered: root.commands.loadTrackToDeck(1) + } + Action { + shortcut: Mixxx.Application.menuShortcut("FileMenu_LoadDeck2", "Ctrl+Shift+O") + text: qsTr("Load Track to Deck &2") + + onTriggered: root.commands.loadTrackToDeck(2) + } + Action { + enabled: root.numberOfDecks >= 3 + text: qsTr("Load Track to Deck &3") + + onTriggered: root.commands.loadTrackToDeck(3) + } + Action { + enabled: root.numberOfDecks >= 4 + text: qsTr("Load Track to Deck &4") + + onTriggered: root.commands.loadTrackToDeck(4) + } + MenuSeparator { + } + Action { + shortcut: Mixxx.Application.menuShortcut("FileMenu_Quit", "Ctrl+Q") + text: qsTr("E&xit") + + onTriggered: Qt.quit() + } + } + Menu { + title: qsTr("&Library") + + Action { + enabled: !Mixxx.Library.libraryScanActive + shortcut: Mixxx.Application.menuShortcut("LibraryMenu_Rescan", "Ctrl+Shift+L") + text: qsTr("&Rescan Library") + + onTriggered: Mixxx.Library.rescanLibrary() + } + Action { + enabled: Mixxx.Library.enginePrimeExportAvailable + text: qsTr("E&xport Library to Engine DJ") + + onTriggered: Mixxx.Library.exportLibrary() + } + MenuSeparator { + } + Action { + shortcut: Mixxx.Application.menuShortcut("LibraryMenu_SearchInCurrentView", "Ctrl+F") + text: qsTr("Search in Current View...") + + onTriggered: { + Mixxx.Library.searchInCurrentView(); + root.focusLibrarySearchRequested(); + } + } + Action { + shortcut: Mixxx.Application.menuShortcut("LibraryMenu_SearchInAllTracks", "Ctrl+Shift+F") + text: qsTr("Search in Tracks Library...") + + onTriggered: { + Mixxx.Library.searchInTracksLibrary(); + root.focusLibrarySearchRequested(); + } + } + MenuSeparator { + } + Action { + shortcut: Mixxx.Application.menuShortcut("LibraryMenu_NewPlaylist", "Ctrl+N") + text: qsTr("Create &New Playlist") + + onTriggered: Mixxx.Library.createPlaylist() + } + Action { + shortcut: Mixxx.Application.menuShortcut("LibraryMenu_NewCrate", "Ctrl+Shift+N") + text: qsTr("Create New &Crate") + + onTriggered: Mixxx.Library.createCrate() + } + } + Menu { + title: qsTr("&View") + "\u200c" + + Action { + checkable: true + checked: showMicrophonesControl.value > 0 + shortcut: Mixxx.Application.menuShortcut("ViewMenu_ShowMicrophone", "Ctrl+2") + text: qsTr("Show Microphone Section") + + onTriggered: showMicrophonesControl.value = showMicrophonesControl.value > 0 ? 0.0 : 1.0 + } + Action { + checkable: true + checked: showVinylControl.value > 0 + shortcut: Mixxx.Application.menuShortcut("ViewMenu_ShowVinylControl", "Ctrl+3") + text: qsTr("Show Vinyl Control Section") + + onTriggered: showVinylControl.value = showVinylControl.value > 0 ? 0.0 : 1.0 + } + Action { + checkable: true + checked: showPreviewDecksControl.value > 0 + shortcut: Mixxx.Application.menuShortcut("ViewMenu_ShowPreviewDeck", "Ctrl+4") + text: qsTr("Show Preview Deck") + + onTriggered: showPreviewDecksControl.value = showPreviewDecksControl.value > 0 ? 0.0 : 1.0 + } + Action { + checkable: true + checked: showLibraryCoverArtControl.value > 0 + shortcut: Mixxx.Application.menuShortcut("ViewMenu_ShowCoverArt", "Ctrl+6") + text: qsTr("Show Cover Art") + + onTriggered: showLibraryCoverArtControl.value = showLibraryCoverArtControl.value > 0 ? 0.0 : 1.0 + } + Action { + shortcut: Mixxx.Application.menuShortcut("ViewMenu_ShowKeywheel", "F12") + text: qsTr("Show Keywheel") + + onTriggered: root.commands.showKeywheel() + } + Action { + checkable: true + checked: showMaximizedLibraryControl.value > 0 + shortcut: Mixxx.Application.menuShortcut("ViewMenu_MaximizeLibrary", "Space") + text: qsTr("Maximize Library") + + onTriggered: showMaximizedLibraryControl.value = showMaximizedLibraryControl.value > 0 ? 0.0 : 1.0 + } + MenuSeparator { + } + Action { + shortcut: Mixxx.Application.menuShortcut("ViewMenu_ShowAutoDJ", "Ctrl+9") + text: qsTr("Show Auto DJ") + + onTriggered: Mixxx.Library.showAutoDJ() + } + Action { + checkable: true + checked: root.applicationWindow.visibility === Window.FullScreen + shortcut: Qt.platform.os === "osx" ? "Ctrl+Meta+F" : "F11" + text: qsTr("&Full Screen") + + onTriggered: { + root.commands.toggleFullScreen(); + } + } + } + Menu { + title: qsTr("&Options") + + Menu { + title: qsTr("Vinyl Control") + + Action { + checkable: true + checked: vinylDeck1Control.value > 0 + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_EnableVinyl1", "Ctrl+T") + text: qsTr("Enable Vinyl Control 1") + + onTriggered: vinylDeck1Control.value = vinylDeck1Control.value > 0 ? 0.0 : 1.0 + } + Action { + checkable: true + checked: vinylDeck2Control.value > 0 + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_EnableVinyl2", "Ctrl+Y") + text: qsTr("Enable Vinyl Control 2") + + onTriggered: vinylDeck2Control.value = vinylDeck2Control.value > 0 ? 0.0 : 1.0 + } + Action { + checkable: true + checked: vinylDeck3Control.value > 0 + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_EnableVinyl3", "Ctrl+U") + text: qsTr("Enable Vinyl Control 3") + + onTriggered: vinylDeck3Control.value = vinylDeck3Control.value > 0 ? 0.0 : 1.0 + } + Action { + checkable: true + checked: vinylDeck4Control.value > 0 + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_EnableVinyl4", "Ctrl+I") + text: qsTr("Enable Vinyl Control 4") + + onTriggered: vinylDeck4Control.value = vinylDeck4Control.value > 0 ? 0.0 : 1.0 + } + } + MenuSeparator { + } + Action { + checkable: true + checked: recordingStatusControl.value > 0 + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_RecordMix", "Ctrl+R") + text: qsTr("&Record Mix") + + onTriggered: recordingToggleControl.trigger() + } + Action { + checkable: true + checked: broadcastEnabledControl.value > 0 + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_EnableLiveBroadcasting", "Ctrl+L") + text: qsTr("Enable Live &Broadcasting") + + onTriggered: broadcastEnabledControl.value = broadcastEnabledControl.value > 0 ? 0.0 : 1.0 + } + Action { + checkable: true + checked: Mixxx.Application.keyboardShortcutsEnabled + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_EnableShortcuts", "Ctrl+`") + text: qsTr("Enable &Keyboard Shortcuts") + + onTriggered: Mixxx.Application.keyboardShortcutsEnabled = !Mixxx.Application.keyboardShortcutsEnabled + } + MenuSeparator { + } + Action { + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_Preferences", "Ctrl+,") + text: qsTr("&Preferences") + + onTriggered: Mixxx.PreferencesDialog.show() + } + } + Menu { + title: qsTr("&Developer") + visible: Mixxx.Application.developerMode + + Action { + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_ReloadSkin", "Ctrl+Shift+R") + text: qsTr("&Reload Skin") + + onTriggered: Mixxx.Application.reloadSkin() + } + + Action { + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_DeveloperTools", "Ctrl+Shift+T") + text: qsTr("Developer &Tools") + + onTriggered: root.commands.showDeveloperToolsRequested() + } + Action { + checkable: true + checked: Mixxx.Application.experimentStatsEnabled + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_DeveloperStatsExperiment", "Ctrl+Shift+E") + text: qsTr("Stats: &Experiment Bucket") + + onTriggered: Mixxx.Application.setExperimentStatsEnabled(!Mixxx.Application.experimentStatsEnabled) + } + Action { + checkable: true + checked: Mixxx.Application.baseStatsEnabled + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_DeveloperStatsBase", "Ctrl+Shift+B") + text: qsTr("Stats: &Base Bucket") + + onTriggered: Mixxx.Application.setBaseStatsEnabled(!Mixxx.Application.baseStatsEnabled) + } + Action { + checkable: true + checked: Mixxx.Application.debuggerEnabled + shortcut: Mixxx.Application.menuShortcut("DeveloperMenu_EnableDebugger", "Ctrl+Shift+D") + text: qsTr("Deb&ugger Enabled") + + onTriggered: Mixxx.Application.debuggerEnabled = !Mixxx.Application.debuggerEnabled + } + } + Menu { + title: qsTr("&Help") + + Action { + text: qsTr("&Community Support") + + onTriggered: Qt.openUrlExternally("https://www.mixxx.org/support/") + } + Action { + text: qsTr("&User Manual") + + onTriggered: Qt.openUrlExternally("https://manual.mixxx.org/2.7/") + } + Action { + text: qsTr("&Keyboard Shortcuts") + + onTriggered: Qt.openUrlExternally("https://manual.mixxx.org/2.7/chapters/controlling_mixxx.html#using-a-keyboard") + } + Action { + text: qsTr("&Settings directory") + + onTriggered: Qt.openUrlExternally(Mixxx.Application.settingsDirectoryUrl) + } + Action { + text: qsTr("&Translate This Application") + + onTriggered: Qt.openUrlExternally("https://explore.transifex.com/mixxx-dj-software/") + } + MenuSeparator { + } + Action { + text: qsTr("&About") + + onTriggered: root.commands.showAbout() + } + } + Mixxx.ControlProxy { + id: showMicrophonesControl + + group: "[Skin]" + key: "show_microphones" + } + Mixxx.ControlProxy { + id: showVinylControl + + group: "[Skin]" + key: "show_vinylcontrol" + } + Mixxx.ControlProxy { + id: showPreviewDecksControl + + group: "[Skin]" + key: "show_preview_decks" + } + Mixxx.ControlProxy { + id: showLibraryCoverArtControl + + group: "[Skin]" + key: "show_library_coverart" + } + Mixxx.ControlProxy { + id: showMaximizedLibraryControl + + group: "[Skin]" + key: "show_maximized_library" + } + Mixxx.ControlProxy { + id: vinylDeck1Control + + group: "[Channel1]" + key: "vinylcontrol_enabled" + } + Mixxx.ControlProxy { + id: vinylDeck2Control + + group: "[Channel2]" + key: "vinylcontrol_enabled" + } + Mixxx.ControlProxy { + id: vinylDeck3Control + + group: "[Channel3]" + key: "vinylcontrol_enabled" + } + Mixxx.ControlProxy { + id: vinylDeck4Control + + group: "[Channel4]" + key: "vinylcontrol_enabled" + } + Mixxx.ControlProxy { + id: recordingStatusControl + + group: "[Recording]" + key: "status" + } + Mixxx.ControlProxy { + id: recordingToggleControl + + group: "[Recording]" + key: "toggle_recording" + } + Mixxx.ControlProxy { + id: broadcastEnabledControl + + group: "[Shoutcast]" + key: "enabled" + } +} diff --git a/res/qml/main.qml b/res/qml/main.qml index a84e747c9316..87844d0fe7cf 100644 --- a/res/qml/main.qml +++ b/res/qml/main.qml @@ -24,12 +24,37 @@ ApplicationWindow { color: Theme.backgroundColor height: 1008 + menuBar: nativeApplicationMenuLoader.item minimumHeight: 300 minimumWidth: 680 visible: true visibility: Mixxx.Config.configStartInFullscreenKey ? Window.FullScreen : Window.Windowed width: 1792 + Loader { + id: nativeApplicationMenuLoader + + active: Qt.platform.os === "osx" + + sourceComponent: Skin.MainMenuBar { + applicationWindow: root + commands: applicationMenuCommands + numberOfDecks: root.numDecks + } + } + Skin.ApplicationMenuCommands { + id: applicationMenuCommands + + applicationWindow: root + + onShowDeveloperToolsRequested: { + devToolsWindow.show(); + devToolsWindow.raise(); + devToolsWindow.requestActivate(); + } + } + Skin.LibraryScanSummaryDialog { + } Mixxx.ControlProxy { group: "[App]" key: "num_decks" From 27dd4e0e957a0ff4415d79f00e9cfcfd2a063711 Mon Sep 17 00:00:00 2001 From: Ayush Sah <75406648+xARSENICx@users.noreply.github.com> Date: Fri, 7 Aug 2026 17:35:22 +0530 Subject: [PATCH 5/8] fix(qml): create developer menu after completion --- res/qml/MainMenuBar.qml | 76 +++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/res/qml/MainMenuBar.qml b/res/qml/MainMenuBar.qml index a026965849f3..8c8739862124 100644 --- a/res/qml/MainMenuBar.qml +++ b/res/qml/MainMenuBar.qml @@ -10,12 +10,20 @@ MenuBar { required property ApplicationWindow applicationWindow required property var commands + property Menu developerMenu: null property int numberOfDecks: 4 signal focusLibrarySearchRequested visible: Qt.platform.os === "osx" + Component.onCompleted: { + if (Mixxx.Application.developerMode) { + developerMenu = developerMenuComponent.createObject(root); + root.insertMenu(root.count - 1, developerMenu); + } + } + Menu { title: qsTr("&File") @@ -245,46 +253,48 @@ MenuBar { onTriggered: Mixxx.PreferencesDialog.show() } } - Menu { - title: qsTr("&Developer") - visible: Mixxx.Application.developerMode + Component { + id: developerMenuComponent - Action { - shortcut: Mixxx.Application.menuShortcut("OptionsMenu_ReloadSkin", "Ctrl+Shift+R") - text: qsTr("&Reload Skin") + Menu { + title: qsTr("&Developer") - onTriggered: Mixxx.Application.reloadSkin() - } + Action { + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_ReloadSkin", "Ctrl+Shift+R") + text: qsTr("&Reload Skin") - Action { - shortcut: Mixxx.Application.menuShortcut("OptionsMenu_DeveloperTools", "Ctrl+Shift+T") - text: qsTr("Developer &Tools") + onTriggered: Mixxx.Application.reloadSkin() + } + Action { + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_DeveloperTools", "Ctrl+Shift+T") + text: qsTr("Developer &Tools") - onTriggered: root.commands.showDeveloperToolsRequested() - } - Action { - checkable: true - checked: Mixxx.Application.experimentStatsEnabled - shortcut: Mixxx.Application.menuShortcut("OptionsMenu_DeveloperStatsExperiment", "Ctrl+Shift+E") - text: qsTr("Stats: &Experiment Bucket") + onTriggered: root.commands.showDeveloperToolsRequested() + } + Action { + checkable: true + checked: Mixxx.Application.experimentStatsEnabled + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_DeveloperStatsExperiment", "Ctrl+Shift+E") + text: qsTr("Stats: &Experiment Bucket") - onTriggered: Mixxx.Application.setExperimentStatsEnabled(!Mixxx.Application.experimentStatsEnabled) - } - Action { - checkable: true - checked: Mixxx.Application.baseStatsEnabled - shortcut: Mixxx.Application.menuShortcut("OptionsMenu_DeveloperStatsBase", "Ctrl+Shift+B") - text: qsTr("Stats: &Base Bucket") + onTriggered: Mixxx.Application.setExperimentStatsEnabled(!Mixxx.Application.experimentStatsEnabled) + } + Action { + checkable: true + checked: Mixxx.Application.baseStatsEnabled + shortcut: Mixxx.Application.menuShortcut("OptionsMenu_DeveloperStatsBase", "Ctrl+Shift+B") + text: qsTr("Stats: &Base Bucket") - onTriggered: Mixxx.Application.setBaseStatsEnabled(!Mixxx.Application.baseStatsEnabled) - } - Action { - checkable: true - checked: Mixxx.Application.debuggerEnabled - shortcut: Mixxx.Application.menuShortcut("DeveloperMenu_EnableDebugger", "Ctrl+Shift+D") - text: qsTr("Deb&ugger Enabled") + onTriggered: Mixxx.Application.setBaseStatsEnabled(!Mixxx.Application.baseStatsEnabled) + } + Action { + checkable: true + checked: Mixxx.Application.debuggerEnabled + shortcut: Mixxx.Application.menuShortcut("DeveloperMenu_EnableDebugger", "Ctrl+Shift+D") + text: qsTr("Deb&ugger Enabled") - onTriggered: Mixxx.Application.debuggerEnabled = !Mixxx.Application.debuggerEnabled + onTriggered: Mixxx.Application.debuggerEnabled = !Mixxx.Application.debuggerEnabled + } } } Menu { From c24b480a725464915c478e31e835e15f4813961d Mon Sep 17 00:00:00 2001 From: Ayush Sah <75406648+xARSENICx@users.noreply.github.com> Date: Fri, 7 Aug 2026 18:47:43 +0530 Subject: [PATCH 6/8] fix(qml): preserve native preferences action --- res/qml/MainMenuBar.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/res/qml/MainMenuBar.qml b/res/qml/MainMenuBar.qml index 8c8739862124..449cfee77cee 100644 --- a/res/qml/MainMenuBar.qml +++ b/res/qml/MainMenuBar.qml @@ -316,7 +316,9 @@ MenuBar { onTriggered: Qt.openUrlExternally("https://manual.mixxx.org/2.7/chapters/controlling_mixxx.html#using-a-keyboard") } Action { - text: qsTr("&Settings directory") + // Qt Quick Controls does not expose native menu roles. Prevent + // macOS from treating this action as the application Preferences action. + text: qsTr("&Settings directory") + "\u200c" onTriggered: Qt.openUrlExternally(Mixxx.Application.settingsDirectoryUrl) } From b8d97012bc2f6545878f5942a7dce002107b1265 Mon Sep 17 00:00:00 2001 From: Ayush Sah <75406648+xARSENICx@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:44:55 +0530 Subject: [PATCH 7/8] fix(qml): fix platform menu handling and deck-dependent actions --- CMakeLists.txt | 3 +++ res/qml/AboutDialog.qml | 9 ++++++++ res/qml/ApplicationMenuCommands.qml | 7 +----- res/qml/MainMenuBar.qml | 4 ++-- res/qml/main.qml | 2 +- src/mixxxmainwindow.cpp | 22 ++----------------- src/qml/qmlapplicationproxy.cpp | 5 +++++ src/qml/qmlapplicationproxy.h | 2 ++ src/qml/qmllibraryproxy.cpp | 30 ++++++++++++-------------- src/qml/qmllibraryproxy.h | 4 +--- src/util/menubarhelper.h | 33 +++++++++++++++++++++++++++++ 11 files changed, 72 insertions(+), 49 deletions(-) create mode 100644 res/qml/AboutDialog.qml create mode 100644 src/util/menubarhelper.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a9122ef381e8..76f910f81611 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4573,6 +4573,9 @@ elseif(UNIX AND NOT APPLE AND NOT EMSCRIPTEN AND NOT ANDROID) endif() find_package(Qt${QT_VERSION_MAJOR} COMPONENTS DBus REQUIRED) target_link_libraries(mixxx-lib PUBLIC Qt${QT_VERSION_MAJOR}::DBus) + if(QML) + target_link_libraries(mixxx-qml-lib PRIVATE Qt${QT_VERSION_MAJOR}::DBus) + endif() elseif(WIN32) if(Qt_IS_STATIC) target_link_libraries( diff --git a/res/qml/AboutDialog.qml b/res/qml/AboutDialog.qml new file mode 100644 index 000000000000..8688ebe0b259 --- /dev/null +++ b/res/qml/AboutDialog.qml @@ -0,0 +1,9 @@ +import Mixxx 1.0 as Mixxx +import QtQuick.Dialogs + +MessageDialog { + buttons: MessageDialog.Ok + informativeText: qsTr("%1\n%2").arg(Mixxx.Application.platform).arg("https://mixxx.org") + text: qsTr("%1 %2").arg(Mixxx.Application.applicationName).arg(Mixxx.Application.version) + title: qsTr("About %1").arg(Mixxx.Application.applicationName) +} diff --git a/res/qml/ApplicationMenuCommands.qml b/res/qml/ApplicationMenuCommands.qml index b4e879ab21bc..d9e493867818 100644 --- a/res/qml/ApplicationMenuCommands.qml +++ b/res/qml/ApplicationMenuCommands.qml @@ -56,13 +56,8 @@ Item { onAccepted: root.openTrackFileDialog() } - MessageDialog { + AboutDialog { id: aboutDialog - - buttons: MessageDialog.Ok - informativeText: qsTr("%1\n%2").arg(Mixxx.Application.platform).arg("https://mixxx.org") - text: qsTr("%1 %2").arg(Mixxx.Application.applicationName).arg(Mixxx.Application.version) - title: qsTr("About %1").arg(Mixxx.Application.applicationName) } Dialog { id: keywheelDialog diff --git a/res/qml/MainMenuBar.qml b/res/qml/MainMenuBar.qml index 449cfee77cee..c17a54027b59 100644 --- a/res/qml/MainMenuBar.qml +++ b/res/qml/MainMenuBar.qml @@ -15,8 +15,6 @@ MenuBar { signal focusLibrarySearchRequested - visible: Qt.platform.os === "osx" - Component.onCompleted: { if (Mixxx.Application.developerMode) { developerMenu = developerMenuComponent.createObject(root); @@ -204,6 +202,7 @@ MenuBar { Action { checkable: true checked: vinylDeck3Control.value > 0 + enabled: root.numberOfDecks >= 3 shortcut: Mixxx.Application.menuShortcut("OptionsMenu_EnableVinyl3", "Ctrl+U") text: qsTr("Enable Vinyl Control 3") @@ -212,6 +211,7 @@ MenuBar { Action { checkable: true checked: vinylDeck4Control.value > 0 + enabled: root.numberOfDecks >= 4 shortcut: Mixxx.Application.menuShortcut("OptionsMenu_EnableVinyl4", "Ctrl+I") text: qsTr("Enable Vinyl Control 4") diff --git a/res/qml/main.qml b/res/qml/main.qml index 87844d0fe7cf..4c367c91f399 100644 --- a/res/qml/main.qml +++ b/res/qml/main.qml @@ -34,7 +34,7 @@ ApplicationWindow { Loader { id: nativeApplicationMenuLoader - active: Qt.platform.os === "osx" + active: Qt.platform.os === "osx" || (Mixxx.Application.supportsGlobalMenuBar && root.visibility !== Window.FullScreen) sourceComponent: Skin.MainMenuBar { applicationWindow: root diff --git a/src/mixxxmainwindow.cpp b/src/mixxxmainwindow.cpp index d787a019d066..35eda8879c07 100644 --- a/src/mixxxmainwindow.cpp +++ b/src/mixxxmainwindow.cpp @@ -53,6 +53,7 @@ #include "track/track.h" #include "util/debug.h" #include "util/desktophelper.h" +#include "util/menubarhelper.h" #include "util/sandbox.h" #include "util/scopedoverridecursor.h" #include "util/timer.h" @@ -69,25 +70,6 @@ #endif namespace { -#ifdef __LINUX__ -// Detect if the desktop supports a global menu to decide whether we need to rebuild -// and reconnect the menu bar when switching to/from fullscreen mode. -// Compared to QMenuBar::isNativeMenuBar() (requires a set menu bar) and -// Qt::AA_DontUseNativeMenuBar, which may both change, this is way more reliable -// since it's rather unlikely that the Appmenu.Registrar service is unloaded/stopped -// while Mixxx is running. -// This is a reimplementation of QGenericUnixTheme > checkDBusGlobalMenuAvailable() -inline bool supportsGlobalMenu() { -#ifndef QT_NO_DBUS - QDBusConnection conn = QDBusConnection::sessionBus(); - if (const auto* pIface = conn.interface()) { - return pIface->isServiceRegistered("com.canonical.AppMenu.Registrar"); - } -#endif - return false; -} -#endif - const ConfigKey kHideMenuBarConfigKey = ConfigKey("[Config]", "hide_menubar"); const ConfigKey kMenuBarHintConfigKey = ConfigKey("[Config]", "show_menubar_hint"); } // namespace @@ -105,7 +87,7 @@ MixxxMainWindow::MixxxMainWindow(std::shared_ptr pCoreServi m_noAuxInputDialog(nullptr), m_pGuiTick(nullptr), #ifdef __LINUX__ - m_supportsGlobalMenuBar(supportsGlobalMenu()), + m_supportsGlobalMenuBar(mixxx::desktopSupportsGlobalMenuBar()), #endif m_inRebootMixxxView(false), m_pDeveloperToolsDlg(nullptr), diff --git a/src/qml/qmlapplicationproxy.cpp b/src/qml/qmlapplicationproxy.cpp index 8b67c079de54..5c79e93c2c4b 100644 --- a/src/qml/qmlapplicationproxy.cpp +++ b/src/qml/qmlapplicationproxy.cpp @@ -5,6 +5,7 @@ #include "preferences/configobject.h" #include "util/cmdlineargs.h" #include "util/experiment.h" +#include "util/menubarhelper.h" #include "util/versionstore.h" namespace { @@ -83,6 +84,10 @@ QString QmlApplicationProxy::platform() const { return VersionStore::platform(); } +bool QmlApplicationProxy::supportsGlobalMenuBar() const { + return desktopSupportsGlobalMenuBar(); +} + void QmlApplicationProxy::setExperimentStatsEnabled(bool enabled) { if (enabled) { Experiment::setExperiment(); diff --git a/src/qml/qmlapplicationproxy.h b/src/qml/qmlapplicationproxy.h index 7e6b20ddd9c2..98eeedfc3118 100644 --- a/src/qml/qmlapplicationproxy.h +++ b/src/qml/qmlapplicationproxy.h @@ -27,6 +27,7 @@ class QmlApplicationProxy : public QObject { Q_PROPERTY(QString applicationName READ applicationName CONSTANT) Q_PROPERTY(QString version READ version CONSTANT) Q_PROPERTY(QString platform READ platform CONSTANT) + Q_PROPERTY(bool supportsGlobalMenuBar READ supportsGlobalMenuBar CONSTANT) QML_NAMED_ELEMENT(Application) QML_SINGLETON @@ -44,6 +45,7 @@ class QmlApplicationProxy : public QObject { QString applicationName() const; QString version() const; QString platform() const; + bool supportsGlobalMenuBar() const; Q_INVOKABLE void setExperimentStatsEnabled(bool enabled); Q_INVOKABLE void setBaseStatsEnabled(bool enabled); diff --git a/src/qml/qmllibraryproxy.cpp b/src/qml/qmllibraryproxy.cpp index 5bf6a2b9614d..14c8eb7ef760 100644 --- a/src/qml/qmllibraryproxy.cpp +++ b/src/qml/qmllibraryproxy.cpp @@ -147,20 +147,18 @@ QmlLibraryScannerProxy::QmlLibraryScannerProxy(LibraryScanner* libraryScanner, Q }); } -QmlLibraryProxy::QmlLibraryProxy( - std::shared_ptr pLibrary, QObject* parent) +QmlLibraryProxy::QmlLibraryProxy(QObject* parent) : QObject(parent), - m_pLibrary(pLibrary), m_pModelProperty(new QmlLibraryTrackListModel( - QList{}, m_pLibrary->trackTableModel(), this)), + QList{}, s_pLibrary->trackTableModel(), this)), m_pScanner(new QmlLibraryScannerProxy( - m_pLibrary->trackCollectionManager()->scanner(), this)) { + s_pLibrary->trackCollectionManager()->scanner(), this)) { connect(m_pScanner, &QmlLibraryScannerProxy::stateChanged, this, &QmlLibraryProxy::libraryScanActiveChanged); TrackCollectionManager* pTrackCollectionManager = - m_pLibrary->trackCollectionManager(); + s_pLibrary->trackCollectionManager(); VERIFY_OR_DEBUG_ASSERT(pTrackCollectionManager) { return; } @@ -223,16 +221,16 @@ QmlLibraryProxy::QmlLibraryProxy( emit libraryScanSummaryAvailable(title, text, details.join(QLatin1Char('\n'))); }); #ifdef __ENGINEPRIME__ - m_pLibraryExporter = m_pLibrary->makeLibraryExporter(nullptr); - connect(m_pLibrary.get(), + m_pLibraryExporter = s_pLibrary->makeLibraryExporter(nullptr); + connect(s_pLibrary.get(), &Library::exportLibrary, m_pLibraryExporter.get(), &mixxx::LibraryExporter::slotRequestExport); - connect(m_pLibrary.get(), + connect(s_pLibrary.get(), &Library::exportCrate, m_pLibraryExporter.get(), &mixxx::LibraryExporter::slotRequestExportWithInitialCrate); - connect(m_pLibrary.get(), + connect(s_pLibrary.get(), &Library::exportPlaylist, m_pLibraryExporter.get(), &mixxx::LibraryExporter::slotRequestExportWithInitialPlaylist); @@ -468,7 +466,7 @@ QmlLibraryProxy* QmlLibraryProxy::create(QQmlEngine* pQmlEngine, QJSEngine* pJsE qWarning() << "Library hasn't been registered yet"; return nullptr; } - return new QmlLibraryProxy(s_pLibrary, pQmlEngine); + return new QmlLibraryProxy(pQmlEngine); } QmlLibraryProxy::AddResult QmlLibraryProxy::addSource( @@ -529,11 +527,10 @@ QmlLibraryProxy::RelocateResult QmlLibraryProxy::relinkSource( // Static qsizetype QmlLibraryProxy::sources_count(QQmlListProperty* pList) { - QmlLibraryProxy* pLibrary = static_cast(pList->object); - VERIFY_OR_DEBUG_ASSERT(pLibrary) { + VERIFY_OR_DEBUG_ASSERT(pList && pList->object && s_pLibrary) { return 0; } - return pLibrary->m_pLibrary->trackCollectionManager() + return s_pLibrary->trackCollectionManager() ->internalCollection() ->getRootDirectories() .size(); @@ -545,12 +542,11 @@ QmlLibrarySource* QmlLibraryProxy::sources_at( VERIFY_OR_DEBUG_ASSERT(pList && pList->object) { return nullptr; } - QmlLibraryProxy* pLibrary = static_cast(pList->object); - VERIFY_OR_DEBUG_ASSERT(pLibrary) { + VERIFY_OR_DEBUG_ASSERT(s_pLibrary) { return nullptr; } return make_qml_owned( - pLibrary->m_pLibrary->trackCollectionManager() + s_pLibrary->trackCollectionManager() ->internalCollection() ->getRootDirectories() .at(index)); diff --git a/src/qml/qmllibraryproxy.h b/src/qml/qmllibraryproxy.h index c18307446892..6cf7bf0fe391 100644 --- a/src/qml/qmllibraryproxy.h +++ b/src/qml/qmllibraryproxy.h @@ -124,7 +124,7 @@ class QmlLibraryProxy : public QObject { }; Q_ENUM(SourceRemovalType); - explicit QmlLibraryProxy(std::shared_ptr pLibrary, QObject* parent = nullptr); + explicit QmlLibraryProxy(QObject* parent = nullptr); ~QmlLibraryProxy() override; static QmlLibraryProxy* create(QQmlEngine* pQmlEngine, QJSEngine* pJsEngine); @@ -200,8 +200,6 @@ class QmlLibraryProxy : public QObject { private: static inline std::shared_ptr s_pLibrary; - std::shared_ptr m_pLibrary; - /// This needs to be a plain pointer because it's used as a `Q_PROPERTY` member variable. QmlLibraryTrackListModel* m_pModelProperty; QmlLibraryScannerProxy* m_pScanner; diff --git a/src/util/menubarhelper.h b/src/util/menubarhelper.h new file mode 100644 index 000000000000..3ba289407c96 --- /dev/null +++ b/src/util/menubarhelper.h @@ -0,0 +1,33 @@ +#pragma once + +#ifdef __LINUX__ +#ifndef QT_NO_DBUS +#include +#include +#include +#endif +#endif + +namespace mixxx { + +// Detect if the desktop supports a global menu to decide whether we need to rebuild +// and reconnect the menu bar when switching to/from fullscreen mode. +// Compared to QMenuBar::isNativeMenuBar() (requires a set menu bar) and +// Qt::AA_DontUseNativeMenuBar, which may both change, this is way more reliable +// since it's rather unlikely that the Appmenu.Registrar service is unloaded/stopped +// while Mixxx is running. +// This is a reimplementation of QGenericUnixTheme > checkDBusGlobalMenuAvailable() +inline bool desktopSupportsGlobalMenuBar() { +#ifdef __LINUX__ +#ifndef QT_NO_DBUS + const QDBusConnection connection = QDBusConnection::sessionBus(); + if (const auto* pInterface = connection.interface()) { + return pInterface->isServiceRegistered( + QStringLiteral("com.canonical.AppMenu.Registrar")); + } +#endif +#endif + return false; +} + +} // namespace mixxx From c044be1242ad4c089a84c8f2c7f82a8ee90814f1 Mon Sep 17 00:00:00 2001 From: Ayush Sah <75406648+xARSENICx@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:26:26 +0530 Subject: [PATCH 8/8] fix(qml): follow active deck layout in application menus --- res/qml/main.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/qml/main.qml b/res/qml/main.qml index 4c367c91f399..63f17133c32b 100644 --- a/res/qml/main.qml +++ b/res/qml/main.qml @@ -39,7 +39,7 @@ ApplicationWindow { sourceComponent: Skin.MainMenuBar { applicationWindow: root commands: applicationMenuCommands - numberOfDecks: root.numDecks + numberOfDecks: root.show4decks ? root.numDecks : 2 } } Skin.ApplicationMenuCommands {