1 #include "widget/wmainmenubar.h"
2 
3 #include <QDesktopServices>
4 #include <QUrl>
5 
6 #include "config.h"
7 #include "control/controlproxy.h"
8 #include "defs_urls.h"
9 #include "mixer/playermanager.h"
10 #include "moc_wmainmenubar.cpp"
11 #include "util/cmdlineargs.h"
12 #include "util/experiment.h"
13 #include "vinylcontrol/defs_vinylcontrol.h"
14 
15 namespace {
16 
17 const int kMaxLoadToDeckActions = 4;
18 
buildWhatsThis(const QString & title,const QString & text)19 QString buildWhatsThis(const QString& title, const QString& text) {
20     QString preparedTitle = title;
21     return QString("%1\n\n%2").arg(preparedTitle.remove("&"), text);
22 }
23 
24 #ifdef __VINYLCONTROL__
vinylControlDefaultKeyBinding(int deck)25 QString vinylControlDefaultKeyBinding(int deck) {
26     // More bindings need to be defined if you increment
27     // kMaximumVinylControlInputs.
28     DEBUG_ASSERT(deck < kMaximumVinylControlInputs);
29     switch (deck) {
30         case 0: return QObject::tr("Ctrl+t");
31         case 1: return QObject::tr("Ctrl+y");
32         case 2: return QObject::tr("Ctrl+u");
33         case 3: return QObject::tr("Ctrl+i");
34         default: return QString();
35     }
36 }
37 #endif // __VINYLCONTROL__
38 
loadToDeckDefaultKeyBinding(int deck)39 QString loadToDeckDefaultKeyBinding(int deck) {
40     switch (deck) {
41         case 0: return QObject::tr("Ctrl+o");
42         case 1: return QObject::tr("Ctrl+Shift+O");
43         default: return QString();
44     }
45 }
46 
showPreferencesKeyBinding()47 QString showPreferencesKeyBinding() {
48 #ifdef __APPLE__
49     return QObject::tr("Ctrl+,");
50 #else
51     return QObject::tr("Ctrl+P");
52 #endif
53 }
54 
documentationUrl(const QString & resourcePath,const QString & fileName,const QString & docUrl)55 QUrl documentationUrl(
56         const QString& resourcePath, const QString& fileName, const QString& docUrl) {
57     QDir resourceDir(resourcePath);
58     // Documentation PDFs are included on Windows and Linux only,
59     // so on macOS this always returns the web URL.
60 #if defined(MIXXX_INSTALL_DOCDIR_RELATIVE_TO_DATADIR)
61     if (!resourceDir.exists(fileName)) {
62         resourceDir.cd(MIXXX_INSTALL_DOCDIR_RELATIVE_TO_DATADIR);
63     }
64 #endif
65     if (resourceDir.exists(fileName)) {
66         return QUrl::fromLocalFile(resourceDir.absoluteFilePath(fileName));
67     } else {
68         return QUrl(docUrl);
69     }
70 }
71 }  // namespace
72 
WMainMenuBar(QWidget * pParent,UserSettingsPointer pConfig,ConfigObject<ConfigValueKbd> * pKbdConfig)73 WMainMenuBar::WMainMenuBar(QWidget* pParent, UserSettingsPointer pConfig,
74                            ConfigObject<ConfigValueKbd>* pKbdConfig)
75         : QMenuBar(pParent),
76           m_pConfig(pConfig),
77           m_pKbdConfig(pKbdConfig) {
78     setObjectName(QStringLiteral("MainMenu"));
79     initialize();
80 }
81 
initialize()82 void WMainMenuBar::initialize() {
83     // FILE MENU
84     QMenu* pFileMenu = new QMenu(tr("&File"), this);
85 
86     QString loadTrackText = tr("Load Track to Deck &%1");
87     QString loadTrackStatusText = tr("Loads a track in deck %1");
88     QString openText = tr("Open");
89     for (unsigned int deck = 0; deck < kMaxLoadToDeckActions; ++deck) {
90         QString playerLoadStatusText = loadTrackStatusText.arg(QString::number(deck + 1));
91         QAction* pFileLoadSongToPlayer = new QAction(
92             loadTrackText.arg(QString::number(deck + 1)), this);
93 
94         QString binding = m_pKbdConfig->getValue(
95                 ConfigKey("[KeyboardShortcuts]", QString("FileMenu_LoadDeck%1").arg(deck + 1)),
96                 loadToDeckDefaultKeyBinding(deck));
97         if (!binding.isEmpty()) {
98             pFileLoadSongToPlayer->setShortcut(QKeySequence(binding));
99             pFileLoadSongToPlayer->setShortcutContext(Qt::ApplicationShortcut);
100         }
101         pFileLoadSongToPlayer->setStatusTip(playerLoadStatusText);
102         pFileLoadSongToPlayer->setWhatsThis(
103             buildWhatsThis(openText, playerLoadStatusText));
104         // Visibility of load to deck actions is set in
105         // WMainMenuBar::onNumberOfDecksChanged.
106         pFileLoadSongToPlayer->setVisible(false);
107         connect(pFileLoadSongToPlayer, &QAction::triggered,
108                 this, [this, deck] { emit loadTrackToDeck(deck + 1); });
109 
110         pFileMenu->addAction(pFileLoadSongToPlayer);
111         m_loadToDeckActions.push_back(pFileLoadSongToPlayer);
112     }
113 
114     pFileMenu->addSeparator();
115 
116     QString quitTitle = tr("&Exit");
117     QString quitText = tr("Quits Mixxx");
118     auto* pFileQuit = new QAction(quitTitle, this);
119     pFileQuit->setShortcut(
120         QKeySequence(m_pKbdConfig->getValue(ConfigKey("[KeyboardShortcuts]", "FileMenu_Quit"),
121                                                   tr("Ctrl+q"))));
122     pFileQuit->setShortcutContext(Qt::ApplicationShortcut);
123     pFileQuit->setStatusTip(quitText);
124     pFileQuit->setWhatsThis(buildWhatsThis(quitTitle, quitText));
125     pFileQuit->setMenuRole(QAction::QuitRole);
126     connect(pFileQuit, &QAction::triggered, this, &WMainMenuBar::quit);
127     pFileMenu->addAction(pFileQuit);
128 
129     addMenu(pFileMenu);
130 
131     // LIBRARY MENU
132     QMenu* pLibraryMenu = new QMenu(tr("&Library"), this);
133 
134     QString rescanTitle = tr("&Rescan Library");
135     QString rescanText = tr("Rescans library folders for changes to tracks.");
136     auto* pLibraryRescan = new QAction(rescanTitle, this);
137     pLibraryRescan->setStatusTip(rescanText);
138     pLibraryRescan->setWhatsThis(buildWhatsThis(rescanTitle, rescanText));
139     pLibraryRescan->setCheckable(false);
140     connect(pLibraryRescan, &QAction::triggered, this, &WMainMenuBar::rescanLibrary);
141     // Disable the action when a scan is active.
142     connect(this, &WMainMenuBar::internalLibraryScanActive, pLibraryRescan, &QAction::setDisabled);
143     pLibraryMenu->addAction(pLibraryRescan);
144 
145     pLibraryMenu->addSeparator();
146 
147     QString createPlaylistTitle = tr("Create &New Playlist");
148     QString createPlaylistText = tr("Create a new playlist");
149     auto* pLibraryCreatePlaylist = new QAction(createPlaylistTitle, this);
150     pLibraryCreatePlaylist->setShortcut(
151         QKeySequence(m_pKbdConfig->getValue(
152                 ConfigKey("[KeyboardShortcuts]", "LibraryMenu_NewPlaylist"),
153                 tr("Ctrl+n"))));
154     pLibraryCreatePlaylist->setShortcutContext(Qt::ApplicationShortcut);
155     pLibraryCreatePlaylist->setStatusTip(createPlaylistText);
156     pLibraryCreatePlaylist->setWhatsThis(buildWhatsThis(createPlaylistTitle, createPlaylistText));
157     connect(pLibraryCreatePlaylist, &QAction::triggered, this, &WMainMenuBar::createPlaylist);
158     pLibraryMenu->addAction(pLibraryCreatePlaylist);
159 
160     QString createCrateTitle = tr("Create New &Crate");
161     QString createCrateText = tr("Create a new crate");
162     auto* pLibraryCreateCrate = new QAction(createCrateTitle, this);
163     pLibraryCreateCrate->setShortcut(
164         QKeySequence(m_pKbdConfig->getValue(ConfigKey("[KeyboardShortcuts]",
165                                                   "LibraryMenu_NewCrate"),
166                                                   tr("Ctrl+Shift+N"))));
167     pLibraryCreateCrate->setShortcutContext(Qt::ApplicationShortcut);
168     pLibraryCreateCrate->setStatusTip(createCrateText);
169     pLibraryCreateCrate->setWhatsThis(buildWhatsThis(createCrateTitle, createCrateText));
170     connect(pLibraryCreateCrate, &QAction::triggered, this, &WMainMenuBar::createCrate);
171     pLibraryMenu->addAction(pLibraryCreateCrate);
172 
173     addMenu(pLibraryMenu);
174 
175 #if defined(__APPLE__)
176     // Note: On macOS 10.11 ff. we have to deal with "automagic" menu items,
177     // when ever a menu "View" is present. QT (as of 5.12.3) does not handle this for us.
178     // Add an invisible suffix to the View item string so it doesn't string-equal "View" ,
179     // and the magic menu items won't get injected.
180     // https://bugs.launchpad.net/mixxx/+bug/1534292
181     QMenu* pViewMenu = new QMenu(tr("&View") + QStringLiteral("\u200C"), this);
182 #else
183     QMenu* pViewMenu = new QMenu(tr("&View"), this);
184 #endif
185 
186     // Skin Settings Menu
187     QString mayNotBeSupported = tr("May not be supported on all skins.");
188     QString showSkinSettingsTitle = tr("Show Skin Settings Menu");
189     QString showSkinSettingsText = tr("Show the Skin Settings Menu of the currently selected Skin") +
190             " " + mayNotBeSupported;
191     auto* pViewShowSkinSettings = new QAction(showSkinSettingsTitle, this);
192     pViewShowSkinSettings->setCheckable(true);
193     pViewShowSkinSettings->setShortcut(
194         QKeySequence(m_pKbdConfig->getValue(
195             ConfigKey("[KeyboardShortcuts]", "ViewMenu_ShowSkinSettings"),
196             tr("Ctrl+1", "Menubar|View|Show Skin Settings"))));
197     pViewShowSkinSettings->setStatusTip(showSkinSettingsText);
198     pViewShowSkinSettings->setWhatsThis(buildWhatsThis(showSkinSettingsTitle, showSkinSettingsText));
199     createVisibilityControl(pViewShowSkinSettings, ConfigKey("[Master]", "skin_settings"));
200     pViewMenu->addAction(pViewShowSkinSettings);
201 
202     // Microphone Section
203     QString showMicrophoneTitle = tr("Show Microphone Section");
204     QString showMicrophoneText = tr("Show the microphone section of the Mixxx interface.") +
205             " " + mayNotBeSupported;
206     auto* pViewShowMicrophone = new QAction(showMicrophoneTitle, this);
207     pViewShowMicrophone->setCheckable(true);
208     pViewShowMicrophone->setShortcut(
209         QKeySequence(m_pKbdConfig->getValue(
210             ConfigKey("[KeyboardShortcuts]", "ViewMenu_ShowMicrophone"),
211             tr("Ctrl+2", "Menubar|View|Show Microphone Section"))));
212     pViewShowMicrophone->setStatusTip(showMicrophoneText);
213     pViewShowMicrophone->setWhatsThis(buildWhatsThis(showMicrophoneTitle, showMicrophoneText));
214     createVisibilityControl(pViewShowMicrophone, ConfigKey("[Microphone]", "show_microphone"));
215     pViewMenu->addAction(pViewShowMicrophone);
216 
217 #ifdef __VINYLCONTROL__
218     QString showVinylControlTitle = tr("Show Vinyl Control Section");
219     QString showVinylControlText = tr("Show the vinyl control section of the Mixxx interface.") +
220             " " + mayNotBeSupported;
221     auto* pViewVinylControl = new QAction(showVinylControlTitle, this);
222     pViewVinylControl->setCheckable(true);
223     pViewVinylControl->setShortcut(
224         QKeySequence(m_pKbdConfig->getValue(
225             ConfigKey("[KeyboardShortcuts]", "ViewMenu_ShowVinylControl"),
226             tr("Ctrl+3", "Menubar|View|Show Vinyl Control Section"))));
227     pViewVinylControl->setStatusTip(showVinylControlText);
228     pViewVinylControl->setWhatsThis(buildWhatsThis(showVinylControlTitle, showVinylControlText));
229     createVisibilityControl(pViewVinylControl, ConfigKey(VINYL_PREF_KEY, "show_vinylcontrol"));
230     pViewMenu->addAction(pViewVinylControl);
231 #endif
232 
233     QString showPreviewDeckTitle = tr("Show Preview Deck");
234     QString showPreviewDeckText = tr("Show the preview deck in the Mixxx interface.") +
235             " " + mayNotBeSupported;
236     auto* pViewShowPreviewDeck = new QAction(showPreviewDeckTitle, this);
237     pViewShowPreviewDeck->setCheckable(true);
238     pViewShowPreviewDeck->setShortcut(
239         QKeySequence(m_pKbdConfig->getValue(
240                 ConfigKey("[KeyboardShortcuts]", "ViewMenu_ShowPreviewDeck"),
241                 tr("Ctrl+4", "Menubar|View|Show Preview Deck"))));
242     pViewShowPreviewDeck->setStatusTip(showPreviewDeckText);
243     pViewShowPreviewDeck->setWhatsThis(buildWhatsThis(showPreviewDeckTitle, showPreviewDeckText));
244     createVisibilityControl(pViewShowPreviewDeck, ConfigKey("[PreviewDeck]", "show_previewdeck"));
245     pViewMenu->addAction(pViewShowPreviewDeck);
246 
247 
248     QString showCoverArtTitle = tr("Show Cover Art");
249     QString showCoverArtText = tr("Show cover art in the Mixxx interface.") +
250             " " + mayNotBeSupported;
251     auto* pViewShowCoverArt = new QAction(showCoverArtTitle, this);
252     pViewShowCoverArt->setCheckable(true);
253     pViewShowCoverArt->setShortcut(
254         QKeySequence(m_pKbdConfig->getValue(
255                 ConfigKey("[KeyboardShortcuts]", "ViewMenu_ShowCoverArt"),
256                 tr("Ctrl+6", "Menubar|View|Show Cover Art"))));
257     pViewShowCoverArt->setStatusTip(showCoverArtText);
258     pViewShowCoverArt->setWhatsThis(buildWhatsThis(showCoverArtTitle, showCoverArtText));
259     createVisibilityControl(pViewShowCoverArt, ConfigKey("[Library]", "show_coverart"));
260     pViewMenu->addAction(pViewShowCoverArt);
261 
262 
263     QString maximizeLibraryTitle = tr("Maximize Library");
264     QString maximizeLibraryText = tr("Maximize the track library to take up all the available screen space.") +
265             " " + mayNotBeSupported;
266     auto* pViewMaximizeLibrary = new QAction(maximizeLibraryTitle, this);
267     pViewMaximizeLibrary->setCheckable(true);
268     pViewMaximizeLibrary->setShortcut(
269         QKeySequence(m_pKbdConfig->getValue(
270                 ConfigKey("[KeyboardShortcuts]", "ViewMenu_MaximizeLibrary"),
271                 tr("Space", "Menubar|View|Maximize Library"))));
272     pViewMaximizeLibrary->setStatusTip(maximizeLibraryText);
273     pViewMaximizeLibrary->setWhatsThis(buildWhatsThis(maximizeLibraryTitle, maximizeLibraryText));
274     createVisibilityControl(pViewMaximizeLibrary, ConfigKey("[Master]", "maximize_library"));
275     pViewMenu->addAction(pViewMaximizeLibrary);
276 
277 
278     pViewMenu->addSeparator();
279 
280 
281     QString fullScreenTitle = tr("&Full Screen");
282     QString fullScreenText = tr("Display Mixxx using the full screen");
283     auto* pViewFullScreen = new QAction(fullScreenTitle, this);
284     QList<QKeySequence> shortcuts;
285     // We use F11 _AND_ the OS shortcut only on Linux and Windows because on
286     // newer macOS versions there might be issues with getting F11 working.
287     // https://github.com/mixxxdj/mixxx/pull/3011#issuecomment-678678328
288 #ifndef __APPLE__
289     shortcuts << QKeySequence("F11");
290 #endif
291     QKeySequence osShortcut = QKeySequence::FullScreen;
292     // Note(ronso0) Only add the OS shortcut if it's not empty and not F11.
293     // In some Linux distros the window managers doesn't pass the OS fullscreen
294     // key sequence to Mixxx for some reason.
295     // Both adding an empty key sequence or the same sequence twice can render
296     // the fullscreen shortcut nonfunctional.
297     // https://bugs.launchpad.net/mixxx/+bug/1882474  PR #3011
298     if (!osShortcut.isEmpty() && !shortcuts.contains(osShortcut)) {
299         shortcuts << osShortcut;
300     }
301 
302     pViewFullScreen->setShortcuts(shortcuts);
303     pViewFullScreen->setShortcutContext(Qt::ApplicationShortcut);
304     pViewFullScreen->setCheckable(true);
305     pViewFullScreen->setChecked(false);
306     pViewFullScreen->setStatusTip(fullScreenText);
307     pViewFullScreen->setWhatsThis(buildWhatsThis(fullScreenTitle, fullScreenText));
308     connect(pViewFullScreen, &QAction::triggered, this, &WMainMenuBar::toggleFullScreen);
309     connect(this,
310             &WMainMenuBar::internalFullScreenStateChange,
311             pViewFullScreen,
312             &QAction::setChecked);
313     pViewMenu->addAction(pViewFullScreen);
314 
315     addMenu(pViewMenu);
316 
317     // OPTIONS MENU
318     QMenu* pOptionsMenu = new QMenu(tr("&Options"), this);
319 
320 #ifdef __VINYLCONTROL__
321     QMenu* pVinylControlMenu = new QMenu(tr("&Vinyl Control"), this);
322     QString vinylControlText = tr(
323             "Use timecoded vinyls on external turntables to control Mixxx");
324 
325     for (int i = 0; i < kMaximumVinylControlInputs; ++i) {
326         QString vinylControlTitle = tr("Enable Vinyl Control &%1").arg(i + 1);
327         auto* vc_checkbox = new QAction(vinylControlTitle, this);
328         m_vinylControlEnabledActions.push_back(vc_checkbox);
329 
330         QString binding = m_pKbdConfig->getValue(
331             ConfigKey("[KeyboardShortcuts]",
332                     QString("OptionsMenu_EnableVinyl%1").arg(i + 1)),
333             vinylControlDefaultKeyBinding(i));
334         if (!binding.isEmpty()) {
335             vc_checkbox->setShortcut(QKeySequence(binding));
336             vc_checkbox->setShortcutContext(Qt::ApplicationShortcut);
337         }
338 
339         // Either check or uncheck the vinyl control menu item depending on what
340         // it was saved as.
341         vc_checkbox->setCheckable(true);
342         vc_checkbox->setChecked(false);
343         // The visibility of these actions is set in
344         // WMainMenuBar::onNumberOfDecksChanged.
345         vc_checkbox->setVisible(false);
346         vc_checkbox->setStatusTip(vinylControlText);
347         vc_checkbox->setWhatsThis(buildWhatsThis(vinylControlTitle,
348                                                  vinylControlText));
349         connect(vc_checkbox, &QAction::triggered,
350                 this, [this, i] { emit toggleVinylControl(i); });
351         pVinylControlMenu->addAction(vc_checkbox);
352     }
353     pOptionsMenu->addMenu(pVinylControlMenu);
354     pOptionsMenu->addSeparator();
355 #endif
356 
357     QString recordTitle = tr("&Record Mix");
358     QString recordText = tr("Record your mix to a file");
359     auto* pOptionsRecord = new QAction(recordTitle, this);
360     pOptionsRecord->setShortcut(
361         QKeySequence(m_pKbdConfig->getValue(
362                 ConfigKey("[KeyboardShortcuts]", "OptionsMenu_RecordMix"),
363                 tr("Ctrl+R"))));
364     pOptionsRecord->setShortcutContext(Qt::ApplicationShortcut);
365     pOptionsRecord->setCheckable(true);
366     pOptionsRecord->setStatusTip(recordText);
367     pOptionsRecord->setWhatsThis(buildWhatsThis(recordTitle, recordText));
368     connect(pOptionsRecord, &QAction::triggered, this, &WMainMenuBar::toggleRecording);
369     connect(this,
370             &WMainMenuBar::internalRecordingStateChange,
371             pOptionsRecord,
372             &QAction::setChecked);
373     pOptionsMenu->addAction(pOptionsRecord);
374 
375 #ifdef __BROADCAST__
376     QString broadcastingTitle = tr("Enable Live &Broadcasting");
377     QString broadcastingText = tr("Stream your mixes to a shoutcast or icecast server");
378     auto* pOptionsBroadcasting = new QAction(broadcastingTitle, this);
379     pOptionsBroadcasting->setShortcut(
380             QKeySequence(m_pKbdConfig->getValue(
381                     ConfigKey("[KeyboardShortcuts]",
382                               "OptionsMenu_EnableLiveBroadcasting"),
383                     tr("Ctrl+L"))));
384     pOptionsBroadcasting->setShortcutContext(Qt::ApplicationShortcut);
385     pOptionsBroadcasting->setCheckable(true);
386     pOptionsBroadcasting->setStatusTip(broadcastingText);
387     pOptionsBroadcasting->setWhatsThis(buildWhatsThis(broadcastingTitle, broadcastingText));
388 
389     connect(pOptionsBroadcasting, &QAction::triggered, this, &WMainMenuBar::toggleBroadcasting);
390     connect(this,
391             &WMainMenuBar::internalBroadcastingStateChange,
392             pOptionsBroadcasting,
393             &QAction::setChecked);
394     pOptionsMenu->addAction(pOptionsBroadcasting);
395 #endif
396 
397     pOptionsMenu->addSeparator();
398 
399     QString keyboardShortcutTitle = tr("Enable &Keyboard Shortcuts");
400     QString keyboardShortcutText = tr("Toggles keyboard shortcuts on or off");
401     bool keyboardShortcutsEnabled = m_pConfig->getValueString(
402         ConfigKey("[Keyboard]", "Enabled")) == "1";
403     auto* pOptionsKeyboard = new QAction(keyboardShortcutTitle, this);
404     pOptionsKeyboard->setShortcut(
405         QKeySequence(m_pKbdConfig->getValue(
406                 ConfigKey("[KeyboardShortcuts]", "OptionsMenu_EnableShortcuts"),
407                 tr("Ctrl+`"))));
408     pOptionsKeyboard->setShortcutContext(Qt::ApplicationShortcut);
409     pOptionsKeyboard->setCheckable(true);
410     pOptionsKeyboard->setChecked(keyboardShortcutsEnabled);
411     pOptionsKeyboard->setStatusTip(keyboardShortcutText);
412     pOptionsKeyboard->setWhatsThis(buildWhatsThis(keyboardShortcutTitle, keyboardShortcutText));
413     connect(pOptionsKeyboard, &QAction::triggered, this, &WMainMenuBar::toggleKeyboardShortcuts);
414 
415     pOptionsMenu->addAction(pOptionsKeyboard);
416 
417     pOptionsMenu->addSeparator();
418 
419     QString preferencesTitle = tr("&Preferences");
420     QString preferencesText = tr("Change Mixxx settings (e.g. playback, MIDI, controls)");
421     auto* pOptionsPreferences = new QAction(preferencesTitle, this);
422     pOptionsPreferences->setShortcut(
423         QKeySequence(m_pKbdConfig->getValue(
424                 ConfigKey("[KeyboardShortcuts]", "OptionsMenu_Preferences"),
425                 showPreferencesKeyBinding())));
426     pOptionsPreferences->setShortcutContext(Qt::ApplicationShortcut);
427     pOptionsPreferences->setStatusTip(preferencesText);
428     pOptionsPreferences->setWhatsThis(buildWhatsThis(preferencesTitle, preferencesText));
429     pOptionsPreferences->setMenuRole(QAction::PreferencesRole);
430     connect(pOptionsPreferences, &QAction::triggered, this, &WMainMenuBar::showPreferences);
431     pOptionsMenu->addAction(pOptionsPreferences);
432 
433     addMenu(pOptionsMenu);
434 
435     // DEVELOPER MENU
436     if (CmdlineArgs::Instance().getDeveloper()) {
437         QMenu* pDeveloperMenu = new QMenu(tr("&Developer"), this);
438 
439         QString reloadSkinTitle = tr("&Reload Skin");
440         QString reloadSkinText = tr("Reload the skin");
441         auto* pDeveloperReloadSkin = new QAction(reloadSkinTitle, this);
442         pDeveloperReloadSkin->setShortcut(
443             QKeySequence(m_pKbdConfig->getValue(
444                     ConfigKey("[KeyboardShortcuts]", "OptionsMenu_ReloadSkin"),
445                     tr("Ctrl+Shift+R"))));
446         pDeveloperReloadSkin->setShortcutContext(Qt::ApplicationShortcut);
447         pDeveloperReloadSkin->setStatusTip(reloadSkinText);
448         pDeveloperReloadSkin->setWhatsThis(buildWhatsThis(reloadSkinTitle, reloadSkinText));
449         connect(pDeveloperReloadSkin, &QAction::triggered, this, &WMainMenuBar::reloadSkin);
450         pDeveloperMenu->addAction(pDeveloperReloadSkin);
451 
452         QString developerToolsTitle = tr("Developer &Tools");
453         QString developerToolsText = tr("Opens the developer tools dialog");
454         auto* pDeveloperTools = new QAction(developerToolsTitle, this);
455         pDeveloperTools->setShortcut(
456             QKeySequence(m_pKbdConfig->getValue(
457                     ConfigKey("[KeyboardShortcuts]", "OptionsMenu_DeveloperTools"),
458                     tr("Ctrl+Shift+T"))));
459         pDeveloperTools->setShortcutContext(Qt::ApplicationShortcut);
460         pDeveloperTools->setCheckable(true);
461         pDeveloperTools->setChecked(false);
462         pDeveloperTools->setStatusTip(developerToolsText);
463         pDeveloperTools->setWhatsThis(buildWhatsThis(developerToolsTitle, developerToolsText));
464         connect(pDeveloperTools, &QAction::triggered, this, &WMainMenuBar::toggleDeveloperTools);
465         connect(this,
466                 &WMainMenuBar::internalDeveloperToolsStateChange,
467                 pDeveloperTools,
468                 &QAction::setChecked);
469         pDeveloperMenu->addAction(pDeveloperTools);
470 
471         QString enableExperimentTitle = tr("Stats: &Experiment Bucket");
472         QString enableExperimentToolsText = tr(
473             "Enables experiment mode. Collects stats in the EXPERIMENT tracking bucket.");
474         auto* pDeveloperStatsExperiment = new QAction(enableExperimentTitle, this);
475         pDeveloperStatsExperiment->setShortcut(
476             QKeySequence(m_pKbdConfig->getValue(
477                     ConfigKey("[KeyboardShortcuts]", "OptionsMenu_DeveloperStatsExperiment"),
478                     tr("Ctrl+Shift+E"))));
479         pDeveloperStatsExperiment->setShortcutContext(Qt::ApplicationShortcut);
480         pDeveloperStatsExperiment->setStatusTip(enableExperimentToolsText);
481         pDeveloperStatsExperiment->setWhatsThis(buildWhatsThis(
482             enableExperimentTitle, enableExperimentToolsText));
483         pDeveloperStatsExperiment->setCheckable(true);
484         pDeveloperStatsExperiment->setChecked(Experiment::isExperiment());
485         connect(pDeveloperStatsExperiment,
486                 &QAction::triggered,
487                 this,
488                 &WMainMenuBar::slotDeveloperStatsExperiment);
489         pDeveloperMenu->addAction(pDeveloperStatsExperiment);
490 
491         QString enableBaseTitle = tr("Stats: &Base Bucket");
492         QString enableBaseToolsText = tr(
493             "Enables base mode. Collects stats in the BASE tracking bucket.");
494         auto* pDeveloperStatsBase = new QAction(enableBaseTitle, this);
495         pDeveloperStatsBase->setShortcut(
496             QKeySequence(m_pKbdConfig->getValue(
497                     ConfigKey("[KeyboardShortcuts]", "OptionsMenu_DeveloperStatsBase"),
498                     tr("Ctrl+Shift+B"))));
499         pDeveloperStatsBase->setShortcutContext(Qt::ApplicationShortcut);
500         pDeveloperStatsBase->setStatusTip(enableBaseToolsText);
501         pDeveloperStatsBase->setWhatsThis(buildWhatsThis(
502             enableBaseTitle, enableBaseToolsText));
503         pDeveloperStatsBase->setCheckable(true);
504         pDeveloperStatsBase->setChecked(Experiment::isBase());
505         connect(pDeveloperStatsBase,
506                 &QAction::triggered,
507                 this,
508                 &WMainMenuBar::slotDeveloperStatsBase);
509         pDeveloperMenu->addAction(pDeveloperStatsBase);
510 
511         // "D" cannont be used with Alt here as it is already by the Developer menu
512         QString scriptDebuggerTitle = tr("Deb&ugger Enabled");
513         QString scriptDebuggerText = tr("Enables the debugger during skin parsing");
514         bool scriptDebuggerEnabled = m_pConfig->getValueString(
515             ConfigKey("[ScriptDebugger]", "Enabled")) == "1";
516         auto* pDeveloperDebugger = new QAction(scriptDebuggerTitle, this);
517         pDeveloperDebugger->setShortcut(
518             QKeySequence(m_pKbdConfig->getValue(
519                     ConfigKey("[KeyboardShortcuts]", "DeveloperMenu_EnableDebugger"),
520                     tr("Ctrl+Shift+D"))));
521         pDeveloperDebugger->setShortcutContext(Qt::ApplicationShortcut);
522         pDeveloperDebugger->setWhatsThis(buildWhatsThis(keyboardShortcutTitle, keyboardShortcutText));
523         pDeveloperDebugger->setCheckable(true);
524         pDeveloperDebugger->setStatusTip(scriptDebuggerText);
525         pDeveloperDebugger->setChecked(scriptDebuggerEnabled);
526         connect(pDeveloperDebugger,
527                 &QAction::triggered,
528                 this,
529                 &WMainMenuBar::slotDeveloperDebugger);
530         pDeveloperMenu->addAction(pDeveloperDebugger);
531 
532         addMenu(pDeveloperMenu);
533     }
534 
535     addSeparator();
536 
537     // HELP MENU
538     QMenu* pHelpMenu = new QMenu(tr("&Help"), this);
539 
540     QString externalLinkSuffix;
541 #ifndef __APPLE__
542     // According to Apple's Human Interface Guidelines devs are encouraged
543     // to not use custom icons in menus.
544     // https://developer.apple.com/design/human-interface-guidelines/macos/menus/menu-anatomy/
545     externalLinkSuffix = QChar(' ') + QChar(0x2197); // north-east arrow
546 #endif
547 
548     // Community Support
549     QString supportTitle = tr("&Community Support") + externalLinkSuffix;
550     QString supportText = tr("Get help with Mixxx");
551     auto* pHelpSupport = new QAction(supportTitle, this);
552     pHelpSupport->setStatusTip(supportText);
553     pHelpSupport->setWhatsThis(buildWhatsThis(supportTitle, supportText));
554     connect(pHelpSupport, &QAction::triggered,
555             this, [this] { slotVisitUrl(MIXXX_SUPPORT_URL); });
556     pHelpMenu->addAction(pHelpSupport);
557 
558     // User Manual
559     QUrl manualUrl = documentationUrl(m_pConfig->getResourcePath(),
560             MIXXX_MANUAL_FILENAME,
561             MIXXX_MANUAL_URL);
562     QString manualSuffix = manualUrl.isLocalFile() ? QString() : externalLinkSuffix;
563 
564     QString manualTitle = tr("&User Manual") + manualSuffix;
565     QString manualText = tr("Read the Mixxx user manual.");
566     auto* pHelpManual = new QAction(manualTitle, this);
567     pHelpManual->setStatusTip(manualText);
568     pHelpManual->setWhatsThis(buildWhatsThis(manualTitle, manualText));
569     connect(pHelpManual, &QAction::triggered, this, [this, manualUrl] {
570         slotVisitUrl(manualUrl.toString());
571     });
572     pHelpMenu->addAction(pHelpManual);
573 
574     // Keyboard Shortcuts
575     QUrl keyboardShortcutsUrl = documentationUrl(m_pConfig->getResourcePath(),
576             MIXXX_KBD_SHORTCUTS_FILENAME,
577             MIXXX_MANUAL_SHORTCUTS_URL);
578     QString keyboardShortcutsSuffix =
579             keyboardShortcutsUrl.isLocalFile() ? QString() : externalLinkSuffix;
580 
581     QString shortcutsTitle = tr("&Keyboard Shortcuts") + keyboardShortcutsSuffix;
582     QString shortcutsText = tr("Speed up your workflow with keyboard shortcuts.");
583     auto* pHelpKbdShortcuts = new QAction(shortcutsTitle, this);
584     pHelpKbdShortcuts->setStatusTip(shortcutsText);
585     pHelpKbdShortcuts->setWhatsThis(buildWhatsThis(shortcutsTitle, shortcutsText));
586     connect(pHelpKbdShortcuts,
587             &QAction::triggered,
588             this,
589             [this, keyboardShortcutsUrl] {
590                 slotVisitUrl(keyboardShortcutsUrl.toString());
591             });
592     pHelpMenu->addAction(pHelpKbdShortcuts);
593 
594     // Translate This Application
595     QString translateTitle = tr("&Translate This Application") + externalLinkSuffix;
596     QString translateText = tr("Help translate this application into your language.");
597     auto* pHelpTranslation = new QAction(translateTitle, this);
598     pHelpTranslation->setStatusTip(translateText);
599     pHelpTranslation->setWhatsThis(buildWhatsThis(translateTitle, translateText));
600     connect(pHelpTranslation, &QAction::triggered,
601             this, [this] { slotVisitUrl(MIXXX_TRANSLATION_URL); });
602     pHelpMenu->addAction(pHelpTranslation);
603 
604     pHelpMenu->addSeparator();
605 
606     QString aboutTitle = tr("&About");
607     QString aboutText = tr("About the application");
608     auto* pHelpAboutApp = new QAction(aboutTitle, this);
609     pHelpAboutApp->setStatusTip(aboutText);
610     pHelpAboutApp->setWhatsThis(buildWhatsThis(aboutTitle, aboutText));
611     pHelpAboutApp->setMenuRole(QAction::AboutRole);
612     connect(pHelpAboutApp, &QAction::triggered, this, &WMainMenuBar::showAbout);
613 
614     pHelpMenu->addAction(pHelpAboutApp);
615     addMenu(pHelpMenu);
616 }
617 
onLibraryScanStarted()618 void WMainMenuBar::onLibraryScanStarted() {
619     emit internalLibraryScanActive(true);
620 }
621 
onLibraryScanFinished()622 void WMainMenuBar::onLibraryScanFinished() {
623     emit internalLibraryScanActive(false);
624 }
625 
onNewSkinLoaded()626 void WMainMenuBar::onNewSkinLoaded() {
627     emit internalOnNewSkinLoaded();
628 }
629 
onNewSkinAboutToLoad()630 void WMainMenuBar::onNewSkinAboutToLoad() {
631     emit internalOnNewSkinAboutToLoad();
632 }
633 
onRecordingStateChange(bool recording)634 void WMainMenuBar::onRecordingStateChange(bool recording) {
635     emit internalRecordingStateChange(recording);
636 }
637 
onBroadcastingStateChange(bool broadcasting)638 void WMainMenuBar::onBroadcastingStateChange(bool broadcasting) {
639     emit internalBroadcastingStateChange(broadcasting);
640 }
641 
onDeveloperToolsShown()642 void WMainMenuBar::onDeveloperToolsShown() {
643     emit internalDeveloperToolsStateChange(true);
644 }
645 
onDeveloperToolsHidden()646 void WMainMenuBar::onDeveloperToolsHidden() {
647     emit internalDeveloperToolsStateChange(false);
648 }
649 
onFullScreenStateChange(bool fullscreen)650 void WMainMenuBar::onFullScreenStateChange(bool fullscreen) {
651     emit internalFullScreenStateChange(fullscreen);
652 }
653 
onVinylControlDeckEnabledStateChange(int deck,bool enabled)654 void WMainMenuBar::onVinylControlDeckEnabledStateChange(int deck, bool enabled) {
655     if (deck < 0 || deck >= m_vinylControlEnabledActions.size()) {
656         DEBUG_ASSERT(false);
657         return;
658     }
659     m_vinylControlEnabledActions.at(deck)->setChecked(enabled);
660 }
661 
slotDeveloperStatsBase(bool enable)662 void WMainMenuBar::slotDeveloperStatsBase(bool enable) {
663     if (enable) {
664         Experiment::setBase();
665     } else {
666         Experiment::disable();
667     }
668 }
669 
slotDeveloperStatsExperiment(bool enable)670 void WMainMenuBar::slotDeveloperStatsExperiment(bool enable) {
671     if (enable) {
672         Experiment::setExperiment();
673     } else {
674         Experiment::disable();
675     }
676 }
677 
slotDeveloperDebugger(bool toggle)678 void WMainMenuBar::slotDeveloperDebugger(bool toggle) {
679     m_pConfig->set(ConfigKey("[ScriptDebugger]","Enabled"),
680                    ConfigValue(toggle ? 1 : 0));
681 }
682 
slotVisitUrl(const QString & url)683 void WMainMenuBar::slotVisitUrl(const QString& url) {
684     QDesktopServices::openUrl(QUrl(url));
685 }
686 
createVisibilityControl(QAction * pAction,const ConfigKey & key)687 void WMainMenuBar::createVisibilityControl(QAction* pAction,
688                                            const ConfigKey& key) {
689     auto* pConnection = new VisibilityControlConnection(this, pAction, key);
690     connect(this,
691             &WMainMenuBar::internalOnNewSkinLoaded,
692             pConnection,
693             &VisibilityControlConnection::slotReconnectControl);
694     connect(this,
695             &WMainMenuBar::internalOnNewSkinAboutToLoad,
696             pConnection,
697             &VisibilityControlConnection::slotClearControl);
698 }
699 
onNumberOfDecksChanged(int decks)700 void WMainMenuBar::onNumberOfDecksChanged(int decks) {
701     int deck = 0;
702     for (QAction* pVinylControlEnabled : qAsConst(m_vinylControlEnabledActions)) {
703         pVinylControlEnabled->setVisible(deck++ < decks);
704     }
705     deck = 0;
706     for (QAction* pLoadToDeck : qAsConst(m_loadToDeckActions)) {
707         pLoadToDeck->setVisible(deck++ < decks);
708     }
709 }
710 
VisibilityControlConnection(QObject * pParent,QAction * pAction,const ConfigKey & key)711 VisibilityControlConnection::VisibilityControlConnection(
712     QObject* pParent, QAction* pAction, const ConfigKey& key)
713         : QObject(pParent),
714           m_key(key),
715           m_pAction(pAction) {
716     connect(m_pAction, &QAction::triggered, this, &VisibilityControlConnection::slotActionToggled);
717 }
718 
slotClearControl()719 void VisibilityControlConnection::slotClearControl() {
720     m_pControl.reset();
721     m_pAction->setEnabled(false);
722 }
723 
slotReconnectControl()724 void VisibilityControlConnection::slotReconnectControl() {
725     m_pControl.reset(new ControlProxy(m_key, this, ControlFlag::NoAssertIfMissing));
726     m_pControl->connectValueChanged(this, &VisibilityControlConnection::slotControlChanged);
727     m_pAction->setEnabled(m_pControl->valid());
728     slotControlChanged();
729 }
730 
slotControlChanged()731 void VisibilityControlConnection::slotControlChanged() {
732     if (m_pControl) {
733         m_pAction->setChecked(m_pControl->toBool());
734     }
735 }
736 
slotActionToggled(bool toggle)737 void VisibilityControlConnection::slotActionToggled(bool toggle) {
738     if (m_pControl) {
739         m_pControl->set(toggle ? 1.0 : 0.0);
740     }
741 }
742