1 #include "preferences/dialog/dlgpreflibrary.h"
2 
3 #include <QApplication>
4 #include <QDesktopServices>
5 #include <QDir>
6 #include <QFileDialog>
7 #include <QFontDialog>
8 #include <QFontMetrics>
9 #include <QMessageBox>
10 #include <QStandardPaths>
11 #include <QStringList>
12 #include <QUrl>
13 
14 #include "library/dlgtrackmetadataexport.h"
15 #include "moc_dlgpreflibrary.cpp"
16 #include "sources/soundsourceproxy.h"
17 #include "widget/wsearchlineedit.h"
18 
19 namespace {
20     const ConfigKey kSearchDebouncingTimeoutMillisKey = ConfigKey("[Library]","SearchDebouncingTimeoutMillis");
21     } // namespace
22 
DlgPrefLibrary(QWidget * pParent,UserSettingsPointer pConfig,Library * pLibrary)23 DlgPrefLibrary::DlgPrefLibrary(
24         QWidget* pParent,
25         UserSettingsPointer pConfig,
26         Library* pLibrary)
27         : DlgPreferencePage(pParent),
28           m_dirListModel(),
29           m_pConfig(pConfig),
30           m_pLibrary(pLibrary),
31           m_bAddedDirectory(false),
32           m_iOriginalTrackTableRowHeight(Library::kDefaultRowHeightPx) {
33     setupUi(this);
34 
35     connect(this,
36             &DlgPrefLibrary::requestAddDir,
37             m_pLibrary,
38             &Library::slotRequestAddDir);
39     connect(this,
40             &DlgPrefLibrary::requestRemoveDir,
41             m_pLibrary,
42             &Library::slotRequestRemoveDir);
43     connect(this,
44             &DlgPrefLibrary::requestRelocateDir,
45             m_pLibrary,
46             &Library::slotRequestRelocateDir);
47     connect(PushButtonAddDir,
48             &QPushButton::clicked,
49             this,
50             &DlgPrefLibrary::slotAddDir);
51     connect(PushButtonRemoveDir,
52             &QPushButton::clicked,
53             this,
54             &DlgPrefLibrary::slotRemoveDir);
55     connect(PushButtonRelocateDir,
56             &QPushButton::clicked,
57             this,
58             &DlgPrefLibrary::slotRelocateDir);
59     connect(checkBox_SeratoMetadataExport,
60             &QAbstractButton::clicked,
61             this,
62             &DlgPrefLibrary::slotSeratoMetadataExportClicked);
63     const QString& settingsDir = m_pConfig->getSettingsPath();
64     connect(PushButtonOpenSettingsDir,
65             &QPushButton::clicked,
66             [settingsDir] {
67                 QDesktopServices::openUrl(QUrl::fromLocalFile(settingsDir));
68             });
69 
70     // Set default direction as stored in config file
71     int rowHeight = m_pLibrary->getTrackTableRowHeight();
72     spinBoxRowHeight->setValue(rowHeight);
73     connect(spinBoxRowHeight,
74             QOverload<int>::of(&QSpinBox::valueChanged),
75             this,
76             &DlgPrefLibrary::slotRowHeightValueChanged);
77 
78     searchDebouncingTimeoutSpinBox->setMinimum(WSearchLineEdit::kMinDebouncingTimeoutMillis);
79     searchDebouncingTimeoutSpinBox->setMaximum(WSearchLineEdit::kMaxDebouncingTimeoutMillis);
80     const auto searchDebouncingTimeoutMillis =
81             m_pConfig->getValue(
82                     ConfigKey("[Library]","SearchDebouncingTimeoutMillis"),
83                     WSearchLineEdit::kDefaultDebouncingTimeoutMillis);
84     searchDebouncingTimeoutSpinBox->setValue(searchDebouncingTimeoutMillis);
85     connect(searchDebouncingTimeoutSpinBox,
86             QOverload<int>::of(&QSpinBox::valueChanged),
87             this,
88             &DlgPrefLibrary::slotSearchDebouncingTimeoutMillisChanged);
89 
90     connect(libraryFontButton, &QAbstractButton::clicked, this, &DlgPrefLibrary::slotSelectFont);
91 
92     // TODO(XXX) this string should be extracted from the soundsources
93     QString builtInFormatsStr = "Ogg Vorbis, FLAC, WAVE, AIFF";
94 #if defined(__MAD__) || defined(__COREAUDIO__)
95     builtInFormatsStr += ", MP3";
96 #endif
97 #if defined(__MEDIAFOUNDATION__) || defined(__COREAUDIO__) || defined(__FAAD__)
98     builtInFormatsStr += ", M4A/MP4";
99 #endif
100 #ifdef __OPUS__
101     builtInFormatsStr += ", Opus";
102 #endif
103 #ifdef __MODPLUG__
104     builtInFormatsStr += ", ModPlug";
105 #endif
106 #ifdef __WV__
107     builtInFormatsStr += ", WavPack";
108 #endif
109     builtInFormats->setText(builtInFormatsStr);
110 
111     connect(checkBox_SyncTrackMetadataExport,
112             &QCheckBox::toggled,
113             this,
114             &DlgPrefLibrary::slotSyncTrackMetadataExportToggled);
115 
116     // Initialize the controls after all slots have been connected
117     slotUpdate();
118 }
119 
slotShow()120 void DlgPrefLibrary::slotShow() {
121     m_bAddedDirectory = false;
122 }
123 
slotHide()124 void DlgPrefLibrary::slotHide() {
125     if (!m_bAddedDirectory) {
126         return;
127     }
128 
129     QMessageBox msgBox;
130     msgBox.setIcon(QMessageBox::Warning);
131     msgBox.setWindowTitle(tr("Music Directory Added"));
132     msgBox.setText(tr("You added one or more music directories. The tracks in "
133                       "these directories won't be available until you rescan "
134                       "your library. Would you like to rescan now?"));
135     QPushButton* scanButton = msgBox.addButton(
136         tr("Scan"), QMessageBox::AcceptRole);
137     msgBox.addButton(QMessageBox::Cancel);
138     msgBox.setDefaultButton(scanButton);
139     msgBox.exec();
140 
141     if (msgBox.clickedButton() == scanButton) {
142         emit scanLibrary();
143         return;
144     }
145 }
146 
helpUrl() const147 QUrl DlgPrefLibrary::helpUrl() const {
148     return QUrl(MIXXX_MANUAL_LIBRARY_URL);
149 }
150 
initializeDirList()151 void DlgPrefLibrary::initializeDirList() {
152     // save which index was selected
153     const QString selected = dirList->currentIndex().data().toString();
154     // clear and fill model
155     m_dirListModel.clear();
156     QStringList dirs = m_pLibrary->getDirs();
157     foreach (QString dir, dirs) {
158         m_dirListModel.appendRow(new QStandardItem(dir));
159     }
160     dirList->setModel(&m_dirListModel);
161     dirList->setCurrentIndex(m_dirListModel.index(0, 0));
162     // reselect index if it still exists
163     for (int i=0 ; i<m_dirListModel.rowCount() ; ++i) {
164         const QModelIndex index = m_dirListModel.index(i, 0);
165         if (index.data().toString() == selected) {
166             dirList->setCurrentIndex(index);
167             break;
168         }
169     }
170 }
171 
slotResetToDefaults()172 void DlgPrefLibrary::slotResetToDefaults() {
173     checkBox_library_scan->setChecked(false);
174     checkBox_SyncTrackMetadataExport->setChecked(false);
175     checkBox_SeratoMetadataExport->setChecked(false);
176     checkBox_use_relative_path->setChecked(false);
177     checkBox_show_rhythmbox->setChecked(true);
178     checkBox_show_banshee->setChecked(true);
179     checkBox_show_itunes->setChecked(true);
180     checkBox_show_traktor->setChecked(true);
181     checkBox_show_rekordbox->setChecked(true);
182     radioButton_dbclick_bottom->setChecked(false);
183     checkBoxEditMetadataSelectedClicked->setChecked(PREF_LIBRARY_EDIT_METADATA_DEFAULT);
184     radioButton_dbclick_top->setChecked(false);
185     radioButton_dbclick_deck->setChecked(true);
186     spinBoxRowHeight->setValue(Library::kDefaultRowHeightPx);
187     setLibraryFont(QApplication::font());
188 }
189 
slotUpdate()190 void DlgPrefLibrary::slotUpdate() {
191     initializeDirList();
192     checkBox_library_scan->setChecked(m_pConfig->getValue(
193             ConfigKey("[Library]","RescanOnStartup"), false));
194     checkBox_SyncTrackMetadataExport->setChecked(m_pConfig->getValue(
195             ConfigKey("[Library]","SyncTrackMetadataExport"), false));
196     checkBox_SeratoMetadataExport->setChecked(m_pConfig->getValue(
197             ConfigKey("[Library]", "SeratoMetadataExport"), false));
198     checkBox_use_relative_path->setChecked(m_pConfig->getValue(
199             ConfigKey("[Library]","UseRelativePathOnExport"), false));
200     checkBox_show_rhythmbox->setChecked(m_pConfig->getValue(
201             ConfigKey("[Library]","ShowRhythmboxLibrary"), true));
202     checkBox_show_banshee->setChecked(m_pConfig->getValue(
203             ConfigKey("[Library]","ShowBansheeLibrary"), true));
204     checkBox_show_itunes->setChecked(m_pConfig->getValue(
205             ConfigKey("[Library]","ShowITunesLibrary"), true));
206     checkBox_show_traktor->setChecked(m_pConfig->getValue(
207             ConfigKey("[Library]","ShowTraktorLibrary"), true));
208     checkBox_show_rekordbox->setChecked(m_pConfig->getValue(
209             ConfigKey("[Library]","ShowRekordboxLibrary"), true));
210     checkBox_show_serato->setChecked(m_pConfig->getValue(
211             ConfigKey("[Library]", "ShowSeratoLibrary"), true));
212 
213     switch (m_pConfig->getValue<int>(
214             ConfigKey("[Library]", "TrackLoadAction"),
215             static_cast<int>(TrackDoubleClickAction::LoadToDeck))) {
216     case static_cast<int>(TrackDoubleClickAction::AddToAutoDJBottom):
217         radioButton_dbclick_bottom->setChecked(true);
218         break;
219     case static_cast<int>(TrackDoubleClickAction::AddToAutoDJTop):
220         radioButton_dbclick_top->setChecked(true);
221         break;
222     case static_cast<int>(TrackDoubleClickAction::Ignore):
223         radioButton_dbclick_ignore->setChecked(true);
224         break;
225     default:
226             radioButton_dbclick_deck->setChecked(true);
227             break;
228     }
229 
230     bool editMetadataSelectedClick = m_pConfig->getValue(
231             ConfigKey("[Library]","EditMetadataSelectedClick"),
232             PREF_LIBRARY_EDIT_METADATA_DEFAULT);
233     checkBoxEditMetadataSelectedClicked->setChecked(editMetadataSelectedClick);
234     m_pLibrary->setEditMedatataSelectedClick(editMetadataSelectedClick);
235 
236     m_originalTrackTableFont = m_pLibrary->getTrackTableFont();
237     m_iOriginalTrackTableRowHeight = m_pLibrary->getTrackTableRowHeight();
238     spinBoxRowHeight->setValue(m_iOriginalTrackTableRowHeight);
239     setLibraryFont(m_originalTrackTableFont);
240 }
241 
slotCancel()242 void DlgPrefLibrary::slotCancel() {
243     // Undo any changes in the library font or row height.
244     m_pLibrary->setFont(m_originalTrackTableFont);
245     m_pLibrary->setRowHeight(m_iOriginalTrackTableRowHeight);
246 }
247 
slotAddDir()248 void DlgPrefLibrary::slotAddDir() {
249     QString fd = QFileDialog::getExistingDirectory(
250         this, tr("Choose a music directory"),
251         QStandardPaths::writableLocation(QStandardPaths::MusicLocation));
252     if (!fd.isEmpty()) {
253         emit requestAddDir(fd);
254         slotUpdate();
255         m_bAddedDirectory = true;
256     }
257 }
258 
slotRemoveDir()259 void DlgPrefLibrary::slotRemoveDir() {
260     QModelIndex index = dirList->currentIndex();
261     QString fd = index.data().toString();
262     QMessageBox removeMsgBox;
263 
264     removeMsgBox.setIcon(QMessageBox::Warning);
265     removeMsgBox.setWindowTitle(tr("Confirm Directory Removal"));
266 
267     removeMsgBox.setText(tr(
268         "Mixxx will no longer watch this directory for new tracks. "
269         "What would you like to do with the tracks from this directory and "
270         "subdirectories?"
271         "<ul>"
272         "<li>Hide all tracks from this directory and subdirectories.</li>"
273         "<li>Delete all metadata for these tracks from Mixxx permanently.</li>"
274         "<li>Leave the tracks unchanged in your library.</li>"
275         "</ul>"
276         "Hiding tracks saves their metadata in case you re-add them in the "
277         "future."));
278     removeMsgBox.setInformativeText(tr(
279         "Metadata means all track details (artist, title, playcount, etc.) as "
280         "well as beatgrids, hotcues, and loops. This choice only affects the "
281         "Mixxx library. No files on disk will be changed or deleted."));
282 
283     QPushButton* cancelButton =
284             removeMsgBox.addButton(QMessageBox::Cancel);
285     QPushButton* hideAllButton = removeMsgBox.addButton(
286         tr("Hide Tracks"), QMessageBox::AcceptRole);
287     QPushButton* deleteAllButton = removeMsgBox.addButton(
288         tr("Delete Track Metadata"), QMessageBox::AcceptRole);
289     QPushButton* leaveUnchangedButton = removeMsgBox.addButton(
290         tr("Leave Tracks Unchanged"), QMessageBox::AcceptRole);
291     Q_UNUSED(leaveUnchangedButton); // Only used in DEBUG_ASSERT
292     removeMsgBox.setDefaultButton(cancelButton);
293     removeMsgBox.exec();
294 
295     if (removeMsgBox.clickedButton() == cancelButton) {
296         return;
297     }
298 
299     Library::RemovalType removalType;
300     if (removeMsgBox.clickedButton() == hideAllButton) {
301         removalType = Library::RemovalType::HideTracks;
302     } else if (removeMsgBox.clickedButton() == deleteAllButton) {
303         removalType = Library::RemovalType::PurgeTracks;
304     } else {
305         DEBUG_ASSERT(removeMsgBox.clickedButton() == leaveUnchangedButton);
306         removalType = Library::RemovalType::KeepTracks;
307     }
308 
309     emit requestRemoveDir(fd, removalType);
310     slotUpdate();
311 }
312 
slotRelocateDir()313 void DlgPrefLibrary::slotRelocateDir() {
314     QModelIndex index = dirList->currentIndex();
315     QString currentFd = index.data().toString();
316 
317     // If the selected directory exists, use it. If not, go up one directory (if
318     // that directory exists). If neither exist, use the default music
319     // directory.
320     QString startDir = currentFd;
321     QDir dir(startDir);
322     if (!dir.exists() && dir.cdUp()) {
323         startDir = dir.absolutePath();
324     } else if (!dir.exists()) {
325         startDir = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
326     }
327 
328     QString fd = QFileDialog::getExistingDirectory(
329         this, tr("Relink music directory to new location"), startDir);
330 
331     if (!fd.isEmpty()) {
332         emit requestRelocateDir(currentFd, fd);
333         slotUpdate();
334     }
335 }
336 
slotSeratoMetadataExportClicked(bool checked)337 void DlgPrefLibrary::slotSeratoMetadataExportClicked(bool checked) {
338     if (checked) {
339         if (QMessageBox::warning(this,
340                     QStringLiteral("Serato Metadata Export"),
341                     QStringLiteral(
342                             "Exporting Serato Metadata from Mixxx is "
343                             "experimental. There is no official documentation "
344                             "of the format. Existing Serato Metadata might be "
345                             "lost and files with Serato metadata written by "
346                             "Mixxx could potentially crash Serato DJ, "
347                             "therefore caution is advised and backups are "
348                             "recommended. Are you sure you want to enable this "
349                             "option?"),
350                     QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
351             checkBox_SeratoMetadataExport->setChecked(false);
352         }
353     }
354 }
355 
slotApply()356 void DlgPrefLibrary::slotApply() {
357     m_pConfig->set(ConfigKey("[Library]","RescanOnStartup"),
358                 ConfigValue((int)checkBox_library_scan->isChecked()));
359     m_pConfig->set(ConfigKey("[Library]","SyncTrackMetadataExport"),
360                 ConfigValue((int)checkBox_SyncTrackMetadataExport->isChecked()));
361     m_pConfig->set(ConfigKey("[Library]", "SeratoMetadataExport"),
362             ConfigValue(static_cast<int>(checkBox_SeratoMetadataExport->isChecked())));
363     m_pConfig->set(ConfigKey("[Library]","UseRelativePathOnExport"),
364                 ConfigValue((int)checkBox_use_relative_path->isChecked()));
365     m_pConfig->set(ConfigKey("[Library]","ShowRhythmboxLibrary"),
366                 ConfigValue((int)checkBox_show_rhythmbox->isChecked()));
367     m_pConfig->set(ConfigKey("[Library]","ShowBansheeLibrary"),
368                 ConfigValue((int)checkBox_show_banshee->isChecked()));
369     m_pConfig->set(ConfigKey("[Library]","ShowITunesLibrary"),
370                 ConfigValue((int)checkBox_show_itunes->isChecked()));
371     m_pConfig->set(ConfigKey("[Library]","ShowTraktorLibrary"),
372                 ConfigValue((int)checkBox_show_traktor->isChecked()));
373     m_pConfig->set(ConfigKey("[Library]","ShowRekordboxLibrary"),
374                 ConfigValue((int)checkBox_show_rekordbox->isChecked()));
375     m_pConfig->set(ConfigKey("[Library]", "ShowSeratoLibrary"),
376             ConfigValue((int)checkBox_show_serato->isChecked()));
377     int dbclick_status;
378     if (radioButton_dbclick_bottom->isChecked()) {
379         dbclick_status = static_cast<int>(TrackDoubleClickAction::AddToAutoDJBottom);
380     } else if (radioButton_dbclick_top->isChecked()) {
381         dbclick_status = static_cast<int>(TrackDoubleClickAction::AddToAutoDJTop);
382     } else if (radioButton_dbclick_deck->isChecked()) {
383         dbclick_status = static_cast<int>(TrackDoubleClickAction::LoadToDeck);
384     } else { // radioButton_dbclick_ignore
385         dbclick_status = static_cast<int>(TrackDoubleClickAction::Ignore);
386     }
387     m_pConfig->set(ConfigKey("[Library]","TrackLoadAction"),
388                 ConfigValue(dbclick_status));
389 
390     m_pConfig->set(ConfigKey("[Library]", "EditMetadataSelectedClick"),
391             ConfigValue(checkBoxEditMetadataSelectedClicked->checkState()));
392     m_pLibrary->setEditMedatataSelectedClick(
393             checkBoxEditMetadataSelectedClicked->checkState());
394 
395     QFont font = m_pLibrary->getTrackTableFont();
396     if (m_originalTrackTableFont != font) {
397         m_pConfig->set(ConfigKey("[Library]", "Font"),
398                        ConfigValue(font.toString()));
399     }
400 
401     int rowHeight = spinBoxRowHeight->value();
402     if (m_iOriginalTrackTableRowHeight != rowHeight) {
403         m_pConfig->set(ConfigKey("[Library]","RowHeight"),
404                        ConfigValue(rowHeight));
405     }
406 
407     // TODO(rryan): Don't save here.
408     m_pConfig->save();
409 }
410 
slotRowHeightValueChanged(int height)411 void DlgPrefLibrary::slotRowHeightValueChanged(int height) {
412     m_pLibrary->setRowHeight(height);
413 }
414 
setLibraryFont(const QFont & font)415 void DlgPrefLibrary::setLibraryFont(const QFont& font) {
416     libraryFont->setText(QString("%1 %2 %3pt").arg(
417         font.family(), font.styleName(), QString::number(font.pointSizeF())));
418     m_pLibrary->setFont(font);
419 
420     // Don't let the font height exceed the row height.
421     QFontMetrics metrics(font);
422     int fontHeight = metrics.height();
423     spinBoxRowHeight->setMinimum(fontHeight);
424     // library.cpp takes care of setting the new row height according to the
425     // previous font height/ row height ratio
426     spinBoxRowHeight->setValue(m_pLibrary->getTrackTableRowHeight());
427 }
428 
slotSelectFont()429 void DlgPrefLibrary::slotSelectFont() {
430     // False if the user cancels font selection.
431     bool ok = false;
432     QFont font = QFontDialog::getFont(&ok, m_pLibrary->getTrackTableFont(),
433                                       this, tr("Select Library Font"));
434     if (ok) {
435         setLibraryFont(font);
436     }
437 }
438 
slotSearchDebouncingTimeoutMillisChanged(int searchDebouncingTimeoutMillis)439 void DlgPrefLibrary::slotSearchDebouncingTimeoutMillisChanged(int searchDebouncingTimeoutMillis) {
440     m_pConfig->setValue(
441             kSearchDebouncingTimeoutMillisKey,
442             searchDebouncingTimeoutMillis);
443     WSearchLineEdit::setDebouncingTimeoutMillis(searchDebouncingTimeoutMillis);
444 }
445 
slotSyncTrackMetadataExportToggled()446 void DlgPrefLibrary::slotSyncTrackMetadataExportToggled() {
447     if (isVisible() && checkBox_SyncTrackMetadataExport->isChecked()) {
448         mixxx::DlgTrackMetadataExport::showMessageBoxOncePerSession();
449     }
450 }
451