-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
Copy pathwtracktableview.cpp
1784 lines (1581 loc) · 63.9 KB
/
wtracktableview.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "widget/wtracktableview.h"
#include <QDrag>
#include <QModelIndex>
#include <QScrollBar>
#include <QShortcut>
#include <QUrl>
#include "control/controlobject.h"
#include "library/dao/trackschema.h"
#include "library/library.h"
#include "library/library_prefs.h"
#include "library/librarytablemodel.h"
#include "library/searchqueryparser.h"
#include "library/trackcollection.h"
#include "library/trackcollectionmanager.h"
#include "mixer/playermanager.h"
#include "moc_wtracktableview.cpp"
#include "preferences/colorpalettesettings.h"
#include "preferences/dialog/dlgprefdeck.h"
#include "preferences/dialog/dlgpreflibrary.h"
#include "sources/soundsourceproxy.h"
#include "track/track.h"
#include "track/trackref.h"
#include "util/assert.h"
#include "util/defs.h"
#include "util/dnd.h"
#include "util/time.h"
#include "widget/wtrackmenu.h"
#include "widget/wtracktableviewheader.h"
namespace {
// ConfigValue key for QTable vertical scrollbar position
const ConfigKey kVScrollBarPosConfigKey{
// mixxx::library::prefs::kConfigGroup is defined in another
// unit of compilation and cannot be reused here!
QStringLiteral("[Library]"),
QStringLiteral("VScrollBarPos")};
} // anonymous namespace
WTrackTableView::WTrackTableView(QWidget* parent,
UserSettingsPointer pConfig,
Library* pLibrary,
double backgroundColorOpacity,
bool sorting)
: WLibraryTableView(parent, pConfig),
m_pConfig(pConfig),
m_pLibrary(pLibrary),
m_backgroundColorOpacity(backgroundColorOpacity),
// Default color for the focus border of TableItemDelegates
m_focusBorderColor(Qt::white),
m_trackPlayedColor(QColor(kDefaultTrackPlayedColor)),
m_trackMissingColor(QColor(kDefaultTrackMissingColor)),
m_sorting(sorting),
m_selectionChangedSinceLastGuiTick(true),
m_loadCachedOnly(false) {
// Connect slots and signals to make the world go 'round.
connect(this, &WTrackTableView::doubleClicked, this, &WTrackTableView::slotMouseDoubleClicked);
m_pCOTGuiTick = new ControlProxy(
QStringLiteral("[App]"), QStringLiteral("gui_tick_50ms_period_s"), this);
m_pCOTGuiTick->connectValueChanged(this, &WTrackTableView::slotGuiTick50ms);
m_pKeyNotation = new ControlProxy(mixxx::library::prefs::kKeyNotationConfigKey, this);
m_pKeyNotation->connectValueChanged(this, &WTrackTableView::keyNotationChanged);
m_pSortColumn = new ControlProxy("[Library]", "sort_column", this);
m_pSortColumn->connectValueChanged(this, &WTrackTableView::applySortingIfVisible);
m_pSortOrder = new ControlProxy("[Library]", "sort_order", this);
m_pSortOrder->connectValueChanged(this, &WTrackTableView::applySortingIfVisible);
connect(this,
&WTrackTableView::scrollValueChanged,
this,
&WTrackTableView::slotScrollValueChanged);
}
WTrackTableView::~WTrackTableView() {
WTrackTableViewHeader* pHeader =
qobject_cast<WTrackTableViewHeader*>(horizontalHeader());
if (pHeader) {
pHeader->saveHeaderState();
}
}
void WTrackTableView::enableCachedOnly() {
if (!m_loadCachedOnly) {
// don't try to load and search covers, drawing only
// covers which are already in the QPixmapCache.
emit onlyCachedCoverArt(true);
m_loadCachedOnly = true;
}
m_lastUserAction = mixxx::Time::elapsed();
}
void WTrackTableView::slotScrollValueChanged(int /*unused*/) {
enableCachedOnly();
}
void WTrackTableView::selectionChanged(
const QItemSelection& selected, const QItemSelection& deselected) {
m_selectionChangedSinceLastGuiTick = true;
enableCachedOnly();
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
// Workaround for Qt6 bug https://bugreports.qt.io/browse/QTBUG-108595:
// If 'selectedClick' is enabled Ctrl+click opens the editor instead of
// toggling the clicked item.
// TODO Remove or adjust version guard as soon as the bug is fixed.
if (m_pLibrary->selectedClickEnabled()) {
if (selectionModel()->selectedRows().size() > 1) {
setSelectedClick(false);
} else {
setSelectedClick(true);
}
}
#endif
QTableView::selectionChanged(selected, deselected);
}
void WTrackTableView::slotGuiTick50ms(double /*unused*/) {
if (!isVisible()) {
// Don't proceed if this isn't visible.
return;
}
// if the user is stopped in the same row for more than 0.1 s,
// we load un-cached cover arts as well.
mixxx::Duration timeDelta = mixxx::Time::elapsed() - m_lastUserAction;
if (m_loadCachedOnly && timeDelta > mixxx::Duration::fromMillis(100)) {
// Show the currently selected track in the large cover art view and
// highlights crate and playlists. Doing this in selectionChanged
// slows down scrolling performance so we wait until the user has
// stopped interacting first.
if (m_selectionChangedSinceLastGuiTick) {
const QModelIndexList indices = getSelectedRows();
if (indices.size() == 1 && indices.first().isValid()) {
// A single track has been selected
TrackModel* pTrackModel = getTrackModel();
if (pTrackModel) {
TrackPointer pTrack = pTrackModel->getTrack(indices.first());
if (pTrack) {
emit trackSelected(pTrack);
}
}
} else {
// None or multiple tracks have been selected
emit trackSelected(TrackPointer());
}
m_selectionChangedSinceLastGuiTick = false;
}
// This allows CoverArtDelegate to request that we load covers from disk
// (as opposed to only serving them from cache).
emit onlyCachedCoverArt(false);
m_loadCachedOnly = false;
}
}
// slot
void WTrackTableView::pasteFromSidebar() {
pasteTracks(QModelIndex());
}
// slot
void WTrackTableView::loadTrackModel(QAbstractItemModel* model, bool restoreState) {
qDebug() << "WTrackTableView::loadTrackModel()" << model;
VERIFY_OR_DEBUG_ASSERT(model) {
return;
}
TrackModel* pTrackModel = dynamic_cast<TrackModel*>(model);
VERIFY_OR_DEBUG_ASSERT(pTrackModel) {
return;
}
// If the model has not changed there's no need to exchange the headers
// which would cause a small GUI freeze
if (getTrackModel() == pTrackModel) {
// Re-sort the table even if the track model is the same. This triggers
// a select() if the table is dirty.
doSortByColumn(horizontalHeader()->sortIndicatorSection(),
horizontalHeader()->sortIndicatorOrder());
if (restoreState) {
restoreCurrentViewState();
}
return;
}
setVisible(false);
// Save the previous track model's header state
WTrackTableViewHeader* oldHeader =
qobject_cast<WTrackTableViewHeader*>(horizontalHeader());
if (oldHeader) {
oldHeader->saveHeaderState();
}
// rryan 12/2009 : Due to a bug in Qt, in order to switch to a model with
// different columns than the old model, we have to create a new horizontal
// header. Also, for some reason the WTrackTableView has to be hidden or
// else problems occur. Since we parent the WtrackTableViewHeader's to the
// WTrackTableView, they are automatically deleted.
auto* header = new WTrackTableViewHeader(Qt::Horizontal, this);
// WTF(rryan) The following saves on unnecessary work on the part of
// WTrackTableHeaderView. setHorizontalHeader() calls setModel() on the
// current horizontal header. If this happens on the old
// WTrackTableViewHeader, then it will save its old state, AND do the work
// of initializing its menus on the new model. We create a new
// WTrackTableViewHeader, so this is wasteful. Setting a temporary
// QHeaderView here saves on setModel() calls. Since we parent the
// QHeaderView to the WTrackTableView, it is automatically deleted.
auto* tempHeader = new QHeaderView(Qt::Horizontal, this);
/* Tobias Rafreider: DO NOT SET SORTING TO TRUE during header replacement
* Otherwise, setSortingEnabled(1) will immediately trigger sortByColumn()
* For some reason this will cause 4 select statements in series
* from which 3 are redundant --> expensive at all
*
* Sorting columns, however, is possible because we
* enable clickable sorting indicators some lines below.
* Furthermore, we connect signal 'sortIndicatorChanged'.
*
* Fixes Bug #672762
*/
setSortingEnabled(false);
setHorizontalHeader(tempHeader);
setModel(model);
setHorizontalHeader(header);
header->setSectionsMovable(true);
header->setSectionsClickable(true);
// Setting this to true would render all column labels BOLD as soon as the
// tableview is focused -- and would not restore the previous style when
// it's unfocused. This can not be overwritten with qss, so it can screw up
// the skin design. Also, due to selectionModel()->selectedRows() it is not
// even useful to indicate the focused column because all columns are highlighted.
header->setHighlightSections(false);
header->setSortIndicatorShown(m_sorting);
header->setDefaultAlignment(Qt::AlignLeft);
// Initialize all column-specific things
for (int i = 0; i < model->columnCount(); ++i) {
// Setup delegates according to what the model tells us
QAbstractItemDelegate* delegate =
pTrackModel->delegateForColumn(i, this);
// We need to delete the old delegates, since the docs say the view will
// not take ownership of them.
QAbstractItemDelegate* old_delegate = itemDelegateForColumn(i);
// If delegate is NULL, it will unset the delegate for the column
setItemDelegateForColumn(i, delegate);
delete old_delegate;
// Show or hide the column based on whether it should be shown or not.
if (pTrackModel->isColumnInternal(i)) {
//qDebug() << "Hiding column" << i;
horizontalHeader()->hideSection(i);
}
/* If Mixxx starts the first time or the header states have been cleared
* due to database schema evolution we gonna hide all columns that may
* contain a potential large number of NULL values. This will hide the
* key column by default unless the user brings it to front
*/
if (pTrackModel->isColumnHiddenByDefault(i) &&
!header->hasPersistedHeaderState()) {
//qDebug() << "Hiding column" << i;
horizontalHeader()->hideSection(i);
}
}
if (m_sorting) {
// NOTE: Should be a UniqueConnection but that requires Qt 4.6
// But Qt::UniqueConnections do not work for lambdas, non-member functions
// and functors; they only apply to connecting to member functions.
// https://doc.qt.io/qt-5/qobject.html#connect
connect(horizontalHeader(),
&QHeaderView::sortIndicatorChanged,
this,
&WTrackTableView::slotSortingChanged,
Qt::AutoConnection);
Qt::SortOrder sortOrder;
TrackModel::SortColumnId sortColumn =
pTrackModel->sortColumnIdFromColumnIndex(
horizontalHeader()->sortIndicatorSection());
if (sortColumn != TrackModel::SortColumnId::Invalid) {
// Sort by the saved sort section and order.
sortOrder = horizontalHeader()->sortIndicatorOrder();
} else {
// No saved order is present. Use the TrackModel's default sort order.
sortColumn = pTrackModel->sortColumnIdFromColumnIndex(pTrackModel->defaultSortColumn());
sortOrder = pTrackModel->defaultSortOrder();
if (sortColumn == TrackModel::SortColumnId::Invalid) {
// If the TrackModel has an invalid or internal column as its default
// sort, find the first valid sort column and sort by that.
const int columnCount = model->columnCount(); // just to avoid an endless while loop
for (int sortColumnIndex = 0; sortColumnIndex < columnCount; sortColumnIndex++) {
sortColumn = pTrackModel->sortColumnIdFromColumnIndex(sortColumnIndex);
if (sortColumn != TrackModel::SortColumnId::Invalid) {
break;
}
}
}
}
m_pSortColumn->set(static_cast<double>(sortColumn));
m_pSortOrder->set(sortOrder);
applySorting();
}
// Set up drag and drop behavior according to whether or not the track
// model says it supports it.
// Defaults
setAcceptDrops(true);
setDragDropMode(QAbstractItemView::DragOnly);
// Always enable drag for now (until we have a model that doesn't support
// this.)
setDragEnabled(true);
if (pTrackModel->hasCapabilities(TrackModel::Capability::ReceiveDrops)) {
setDragDropMode(QAbstractItemView::DragDrop);
setDropIndicatorShown(true);
setAcceptDrops(true);
//viewport()->setAcceptDrops(true);
}
// Possible giant fuckup alert - It looks like Qt has something like these
// caps built-in, see http://doc.trolltech.com/4.5/qt.html#ItemFlag-enum and
// the flags(...) function that we're already using in LibraryTableModel. I
// haven't been able to get it to stop us from using a model as a drag
// target though, so my hacks above may not be completely unjustified.
setVisible(true);
// trigger restoring scrollBar position, selection etc.
if (restoreState) {
restoreCurrentViewState();
}
initTrackMenu();
}
void WTrackTableView::initTrackMenu() {
auto* pTrackModel = getTrackModel();
DEBUG_ASSERT(pTrackModel);
if (m_pTrackMenu) {
m_pTrackMenu->deleteLater();
}
m_pTrackMenu = make_parented<WTrackMenu>(this,
m_pConfig,
m_pLibrary,
WTrackMenu::Feature::All,
pTrackModel);
connect(m_pTrackMenu.get(),
&WTrackMenu::loadTrackToPlayer,
this,
&WLibraryTableView::loadTrackToPlayer);
connect(m_pTrackMenu,
&WTrackMenu::trackMenuVisible,
this,
[this](bool visible) {
emit trackMenuVisible(visible);
});
// after removing tracks from the view via track menu, restore a usable
// selection/currentIndex for navigation via keyboard & controller
connect(m_pTrackMenu,
&WTrackMenu::restoreCurrentViewStateOrIndex,
this,
&WTrackTableView::slotrestoreCurrentIndex);
}
// slot
void WTrackTableView::slotMouseDoubleClicked(const QModelIndex& index) {
// Read the current TrackDoubleClickAction setting
// TODO simplify this casting madness
int doubleClickActionConfigValue =
m_pConfig->getValue(mixxx::library::prefs::kTrackDoubleClickActionConfigKey,
static_cast<int>(DlgPrefLibrary::TrackDoubleClickAction::LoadToDeck));
DlgPrefLibrary::TrackDoubleClickAction doubleClickAction =
static_cast<DlgPrefLibrary::TrackDoubleClickAction>(
doubleClickActionConfigValue);
if (doubleClickAction == DlgPrefLibrary::TrackDoubleClickAction::Ignore) {
return;
}
auto* pTrackModel = getTrackModel();
VERIFY_OR_DEBUG_ASSERT(pTrackModel) {
return;
}
if (doubleClickAction == DlgPrefLibrary::TrackDoubleClickAction::LoadToDeck &&
pTrackModel->hasCapabilities(
TrackModel::Capability::LoadToDeck)) {
TrackPointer pTrack = pTrackModel->getTrack(index);
if (pTrack) {
emit loadTrack(pTrack);
}
} else if (doubleClickAction == DlgPrefLibrary::TrackDoubleClickAction::AddToAutoDJBottom &&
pTrackModel->hasCapabilities(
TrackModel::Capability::AddToAutoDJ)) {
addToAutoDJ(PlaylistDAO::AutoDJSendLoc::BOTTOM);
} else if (doubleClickAction == DlgPrefLibrary::TrackDoubleClickAction::AddToAutoDJTop &&
pTrackModel->hasCapabilities(
TrackModel::Capability::AddToAutoDJ)) {
addToAutoDJ(PlaylistDAO::AutoDJSendLoc::TOP);
}
}
TrackModel::SortColumnId WTrackTableView::getColumnIdFromCurrentIndex() {
TrackModel* pTrackModel = getTrackModel();
VERIFY_OR_DEBUG_ASSERT(pTrackModel) {
return TrackModel::SortColumnId::Invalid;
}
return pTrackModel->sortColumnIdFromColumnIndex(currentIndex().column());
}
void WTrackTableView::assignPreviousTrackColor() {
TrackModel* pTrackModel = getTrackModel();
if (!pTrackModel) {
return;
}
const QModelIndexList indices = getSelectedRows();
if (indices.isEmpty()) {
return;
}
QModelIndex index = indices.at(0);
TrackPointer pTrack = pTrackModel->getTrack(index);
if (pTrack) {
ColorPaletteSettings colorPaletteSettings(m_pConfig);
ColorPalette colorPalette = colorPaletteSettings.getTrackColorPalette();
mixxx::RgbColor::optional_t color = pTrack->getColor();
pTrack->setColor(colorPalette.previousColor(color));
}
}
void WTrackTableView::assignNextTrackColor() {
TrackModel* pTrackModel = getTrackModel();
if (!pTrackModel) {
return;
}
const QModelIndexList indices = getSelectedRows();
if (indices.isEmpty()) {
return;
}
QModelIndex index = indices.at(0);
TrackPointer pTrack = pTrackModel->getTrack(index);
if (pTrack) {
ColorPaletteSettings colorPaletteSettings(m_pConfig);
ColorPalette colorPalette = colorPaletteSettings.getTrackColorPalette();
mixxx::RgbColor::optional_t color = pTrack->getColor();
pTrack->setColor(colorPalette.nextColor(color));
}
}
void WTrackTableView::slotPurge() {
TrackModel* pTrackModel = getTrackModel();
if (!pTrackModel) {
return;
}
const QModelIndexList indices = getSelectedRows();
if (indices.isEmpty()) {
return;
}
saveCurrentIndex();
pTrackModel->purgeTracks(indices);
restoreCurrentIndex();
}
void WTrackTableView::slotDeleteTracksFromDisk() {
const QModelIndexList indices = getSelectedRows();
if (indices.isEmpty()) {
return;
}
saveCurrentIndex();
m_pTrackMenu->loadTrackModelIndices(indices);
m_pTrackMenu->slotRemoveFromDisk();
// WTrackmenu emits restoreCurrentViewStateOrIndex()
}
void WTrackTableView::slotUnhide() {
TrackModel* pTrackModel = getTrackModel();
if (!pTrackModel) {
return;
}
const QModelIndexList indices = getSelectedRows();
if (indices.isEmpty()) {
return;
}
saveCurrentIndex();
pTrackModel->unhideTracks(indices);
restoreCurrentIndex();
}
void WTrackTableView::slotShowHideTrackMenu(bool show) {
VERIFY_OR_DEBUG_ASSERT(m_pTrackMenu.get()) {
return;
}
if (show == m_pTrackMenu->isVisible()) {
emit trackMenuVisible(show);
return;
}
if (show) {
QContextMenuEvent event(QContextMenuEvent::Mouse,
mapFromGlobal(QCursor::pos()),
QCursor::pos());
contextMenuEvent(&event);
} else {
m_pTrackMenu->close();
}
}
void WTrackTableView::contextMenuEvent(QContextMenuEvent* event) {
VERIFY_OR_DEBUG_ASSERT(m_pTrackMenu.get()) {
initTrackMenu();
}
event->accept();
// Update track indices in context menu
const QModelIndexList indices = getSelectedRows();
if (indices.isEmpty()) {
return;
}
// TODO Also pass the index of the focused column so DlgTrackInfo/~Multi?
// They could then focus the respective edit field.
m_pTrackMenu->loadTrackModelIndices(indices);
const QModelIndex clickedIdx = indexAt(event->pos());
m_pTrackMenu->setTrackPropertyName(columnNameOfIndex(clickedIdx));
saveCurrentIndex();
m_pTrackMenu->popup(event->globalPos());
// WTrackmenu emits restoreCurrentViewStateOrIndex() if required
}
QString WTrackTableView::columnNameOfIndex(const QModelIndex& index) const {
if (!index.isValid()) {
return {};
}
VERIFY_OR_DEBUG_ASSERT(model()) {
return {};
}
return model()->headerData(
index.column(),
Qt::Horizontal,
TrackModel::kHeaderNameRole)
.toString();
}
void WTrackTableView::onSearch(const QString& text) {
TrackModel* pTrackModel = getTrackModel();
if (!pTrackModel) {
return;
}
saveCurrentViewState();
bool queryIsLessSpecific = SearchQueryParser::queryIsLessSpecific(
pTrackModel->currentSearch(), text);
QList<TrackId> selectedTracks = getSelectedTrackIds();
TrackId prevTrack = getCurrentTrackId();
saveCurrentIndex();
pTrackModel->search(text);
if (queryIsLessSpecific) {
// If the user removed query terms, we try to select the same
// tracks as before
setCurrentTrackId(prevTrack, m_prevColumn);
setSelectedTracks(selectedTracks);
} else {
// The user created a more specific search query, try to restore a
// previous state
if (!restoreCurrentViewState()) {
// We found no saved state for this query, try to select the
// tracks last active, if they are part of the result set
if (!setCurrentTrackId(prevTrack, m_prevColumn)) {
// if the last focused track is not present try to focus the
// respective index and scroll there
restoreCurrentIndex();
}
setSelectedTracks(selectedTracks);
}
}
}
void WTrackTableView::onShow() {
}
void WTrackTableView::mousePressEvent(QMouseEvent* pEvent) {
DragAndDropHelper::mousePressed(pEvent);
WLibraryTableView::mousePressEvent(pEvent);
}
void WTrackTableView::mouseMoveEvent(QMouseEvent* pEvent) {
// Only use this for drag and drop if the LeftButton is pressed we need to
// check for this because mousetracking is activated and this function is
// called every time the mouse is moved -- kain88 May 2012
if (pEvent->buttons() != Qt::LeftButton) {
// Needed for mouse-tracking to fire entered() events. If we call this
// outside of this if statement then we get 'ghost' drags. See issue
// #6507
WLibraryTableView::mouseMoveEvent(pEvent);
return;
}
TrackModel* pTrackModel = getTrackModel();
if (!pTrackModel) {
return;
}
//qDebug() << "MouseMoveEvent";
if (DragAndDropHelper::mouseMoveInitiatesDrag(pEvent)) {
// Iterate over selected rows and append each item's location url to a list.
QList<QString> locations;
const QModelIndexList indices = getSelectedRows();
for (const QModelIndex& index : indices) {
if (!index.isValid()) {
continue;
}
locations.append(pTrackModel->getTrackLocation(index));
}
DragAndDropHelper::dragTrackLocations(locations, this, "library");
}
}
// Drag enter event, happens when a dragged item hovers over the track table view
void WTrackTableView::dragEnterEvent(QDragEnterEvent * event) {
auto* pTrackModel = getTrackModel();
if (!pTrackModel || !event->mimeData()->hasUrls()) {
event->ignore();
return;
}
//qDebug() << "dragEnterEvent" << event->mimeData()->formats();
if (event->source() == this) {
if (pTrackModel->hasCapabilities(TrackModel::Capability::Reorder)) {
event->acceptProposedAction();
}
} else if (DragAndDropHelper::dragEnterAccept(*event->mimeData(),
"library",
true,
true)) {
event->acceptProposedAction();
}
}
// Drag move event, happens when a dragged item hovers over the track table view...
// It changes the drop handle to a "+" when the drag content is acceptable.
// Without it, the following drop is ignored.
void WTrackTableView::dragMoveEvent(QDragMoveEvent * event) {
auto* pTrackModel = getTrackModel();
if (!pTrackModel) {
return;
}
// Needed to allow auto-scrolling
WLibraryTableView::dragMoveEvent(event);
//qDebug() << "dragMoveEvent" << event->mimeData()->formats();
if (pTrackModel && event->mimeData()->hasUrls()) {
if (event->source() == this) {
if (pTrackModel->hasCapabilities(TrackModel::Capability::Reorder)) {
event->acceptProposedAction();
} else {
event->ignore();
}
} else {
event->acceptProposedAction();
}
} else {
event->ignore();
}
}
// Drag-and-drop "drop" event. Occurs when something is dropped onto the track table view
void WTrackTableView::dropEvent(QDropEvent * event) {
TrackModel* pTrackModel = getTrackModel();
// We only do things to the TrackModel in this method so if we don't have
// one we should just bail.
if (!pTrackModel) {
event->ignore();
return;
}
QItemSelectionModel* pSelectionModel = selectionModel();
VERIFY_OR_DEBUG_ASSERT(pSelectionModel != nullptr) {
qWarning() << "No selection model available";
event->ignore();
return;
}
if (!event->mimeData()->hasUrls() || pTrackModel->isLocked()) {
event->ignore();
return;
}
// Save the vertical scrollbar position. Adding new tracks and moving tracks in
// the SQL data models causes a select() (ie. generation of a new result set),
// which causes view to reset itself. A view reset causes the widget to scroll back
// up to the top, which is confusing when you're dragging and dropping. :)
int vScrollBarPos = verticalScrollBar()->value();
// Calculate the model index where the track or tracks are destined to go.
// (the "drop" position in a drag-and-drop)
// The user usually drops on the seam between two rows.
// We take the row below the seam for reference.
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QPoint position = event->position().toPoint();
#else
QPoint position = event->pos();
#endif
int dropRow = rowAt(position.y());
int height = rowHeight(dropRow);
QPoint pointOfRowBelowSeam(position.x(), position.y() + height / 2);
QModelIndex destIndex = indexAt(pointOfRowBelowSeam);
//qDebug() << "destIndex.row() is" << destIndex.row();
// Drag and drop within this widget (track reordering)
if (event->source() == this &&
pTrackModel->hasCapabilities(TrackModel::Capability::Reorder)) {
// Note the above code hides an ambiguous case when a
// playlist is empty. For that reason, we can't factor that
// code out to be common for both internal reordering
// and external drag-and-drop. With internal reordering,
// you can't have an empty playlist. :)
// Save a list of rows (just plain ints) so we don't get screwed over
// when the QModelIndexes all become invalid (eg. after moveTrack()
// or addTrack())
QList<int> selectedRows = getSelectedRowNumbers();
if (selectedRows.isEmpty()) {
return;
}
moveRows(selectedRows, destIndex.row());
} else { // Drag and drop inside Mixxx is only for few rows, bulks happen here
// Reset the selected tracks (if you had any tracks highlighted, it
// clears them)
pSelectionModel->clear();
// Have to do this here because the index is invalid after
// addTrack
int selectionStartRow = destIndex.row();
// Make a new selection starting from where the first track was
// dropped, and select all the dropped tracks
// If the track was dropped into an empty playlist, start at row
// 0 not -1 :)
if ((destIndex.row() == -1) && (model()->rowCount() == 0)) {
selectionStartRow = 0;
} else if ((destIndex.row() == -1) && (model()->rowCount() > 0)) {
// If the track was dropped beyond the end of a playlist, then
// we need to fudge the destination a bit...
//qDebug() << "Beyond end of playlist";
//qDebug() << "rowcount is:" << model()->rowCount();
selectionStartRow = model()->rowCount();
}
// Add all the dropped URLs/tracks to the track model (playlist/crate)
int numNewRows;
{
const QList<mixxx::FileInfo> trackFileInfos =
DragAndDropHelper::supportedTracksFromUrls(
event->mimeData()->urls(), false, true);
QList<QString> trackLocations;
trackLocations.reserve(trackFileInfos.size());
for (const auto& fileInfo : trackFileInfos) {
trackLocations.append(fileInfo.location());
}
numNewRows = pTrackModel->addTracks(destIndex, trackLocations);
DEBUG_ASSERT(numNewRows >= 0);
DEBUG_ASSERT(numNewRows <= trackFileInfos.size());
}
// Create the selection, but only if the track model supports
// reordering. (eg. crates don't support reordering/indexes)
if (pTrackModel->hasCapabilities(TrackModel::Capability::Reorder)) {
// TODO Also set current index to have good starting point for navigation?
for (int i = selectionStartRow; i < selectionStartRow + numNewRows; i++) {
pSelectionModel->select(model()->index(i, 0),
QItemSelectionModel::Select |
QItemSelectionModel::Rows);
}
}
}
event->acceptProposedAction();
updateGeometries();
verticalScrollBar()->setValue(vScrollBarPos);
}
QModelIndexList WTrackTableView::getSelectedRows() const {
if (getTrackModel() == nullptr) {
return {};
}
QItemSelectionModel* pSelectionModel = selectionModel();
VERIFY_OR_DEBUG_ASSERT(pSelectionModel != nullptr) {
qWarning() << "No selection model available";
return {};
}
return pSelectionModel->selectedRows();
}
QList<int> WTrackTableView::getSelectedRowNumbers() const {
const QModelIndexList indices = getSelectedRows();
QList<int> selectedRows;
for (const QModelIndex& idx : indices) {
selectedRows.append(idx.row());
}
std::sort(selectedRows.begin(), selectedRows.end());
return selectedRows;
}
TrackModel* WTrackTableView::getTrackModel() const {
TrackModel* pTrackModel = dynamic_cast<TrackModel*>(model());
return pTrackModel;
}
namespace {
QModelIndex calculateCutIndex(const QModelIndex& currentIndex,
const QModelIndexList& removedIndices) {
if (removedIndices.empty()) {
return QModelIndex();
}
const int row = currentIndex.row();
int rowAfterRemove = row;
for (const auto& removeIndex : removedIndices) {
if (removeIndex.row() < row) {
rowAfterRemove--;
}
}
return currentIndex.siblingAtRow(rowAfterRemove);
}
} // namespace
void WTrackTableView::removeSelectedTracks() {
const QModelIndexList indices = getSelectedRows();
const QModelIndex newIndex = calculateCutIndex(currentIndex(), indices);
getTrackModel()->removeTracks(indices);
setCurrentIndex(newIndex);
}
void WTrackTableView::cutSelectedTracks() {
const QModelIndexList indices = getSelectedRows();
const QModelIndex newIndex = calculateCutIndex(currentIndex(), indices);
getTrackModel()->cutTracks(indices);
setCurrentIndex(newIndex);
}
void WTrackTableView::copySelectedTracks() {
const QModelIndexList indices = getSelectedRows();
getTrackModel()->copyTracks(indices);
}
void WTrackTableView::pasteTracks(const QModelIndex& index) {
TrackModel* trackModel = getTrackModel();
if (!trackModel) {
return;
}
const auto prevIdx = currentIndex();
const QList<int> rows = trackModel->pasteTracks(index);
if (rows.empty()) {
return;
}
updateGeometries();
const auto lastVisibleRow = rowAt(height());
// Use selectRow to scroll to the first or last pasted row. We would use
// scrollTo but this is broken. This solution was already used elsewhere
// in this way.
if (rows.back() > lastVisibleRow) {
selectRow(rows.back());
} else {
selectRow(rows.front());
}
const auto idx = prevIdx.siblingAtRow(rows.back());
QItemSelectionModel* pSelectionModel = selectionModel();
if (pSelectionModel && idx.isValid()) {
pSelectionModel->setCurrentIndex(idx,
QItemSelectionModel::SelectCurrent | QItemSelectionModel::Select);
}
// Select all the rows that we pasted
for (const auto row : rows) {
selectionModel()->select(model()->index(row, 0),
QItemSelectionModel::Select | QItemSelectionModel::Rows);
}
}
void WTrackTableView::moveRows(QList<int> selectedRowsIn, int destRow) {
TrackModel* pTrackModel = getTrackModel();
if (!pTrackModel) {
return;
}
if (selectedRowsIn.isEmpty()) {
return;
}
// Note(RRyan/Max Linke):
// The biggest subtlety in the way I've done this track reordering code
// is that as soon as we've moved ANY track, all of our QModelIndexes probably
// get screwed up. The starting point for the logic below is to say screw
// the QModelIndexes, and just keep a list of row numbers to work from.
// That ends up making the logic simpler and the behavior totally predictable,
// which lets us do nice things like "restore" the selection model.
// The model indices are sorted so that we remove the tracks from the table
// in ascending order. This is necessary because if track A is above track B in
// the table, and you remove track A, the model index for track B will change.
// Sorting the indices first means we don't have to worry about this.
QList<int> selectedRows = std::move(selectedRowsIn);
// An invalid destination row means we're supposed to move the selection to the end.
// Happens when we drop tracks into the void below the last track.
destRow = destRow < 0 ? model()->rowCount() : destRow;
// Required for refocusing the correct column and restoring the selection
// after we moved. Use 0 if the index is invalid for some reason.
int idxCol = std::max(0, currentIndex().column());
int selectedRowCount = selectedRows.count();
int selectionRestoreStartRow = destRow;
int firstSelRow = selectedRows.first();
int lastSelRow = selectedRows.last();
if (destRow == firstSelRow && selectedRowCount == 1) {
return; // no-op
}
// Adjust first row of new selection
if (destRow >= firstSelRow && destRow <= lastSelRow) {
// Destination is inside the selection.
if (selectedRowCount == lastSelRow - firstSelRow + 1) {
// If we drag a contiguous selection of multiple tracks and drop them
// somewhere inside that same selection, we obviously have nothing to do.
// This is also a good way to abort accidental drags.
return;
}
// Non-continuous selection:
if (destRow == firstSelRow) {
// Consolidate selection at first selected row.
// Remove consecutive rows (they are already in place) until we find
// the first gap in the selection.
// Use the row after that continuous part as destination.
while (destRow == firstSelRow) {
selectedRows.removeFirst();
firstSelRow = selectedRows.first();
destRow++;
}
} else {
return;
}
}
if (destRow < firstSelRow) {
// If we're moving the tracks UP, reverse the order of the row selection
// to make the algorithm below work as it is
std::sort(selectedRows.begin(),
selectedRows.end(),
std::greater<int>());
} else { // Down
if (destRow > lastSelRow) {
// If we're moving the tracks DOWN, adjust the first row to reselect
selectionRestoreStartRow =
selectionRestoreStartRow - selectedRowCount;
}
}
// For each row that needs to be moved...
while (!selectedRows.isEmpty()) {
int movedRow = selectedRows.takeFirst(); // Remember it's row index
// Move it
pTrackModel->moveTrack(model()->index(movedRow, 0), model()->index(destRow, 0));
// Move the row indices for rows that got bumped up
// into the void we left, or down because of the new spot
// we're taking.
for (int i = 0; i < selectedRows.count(); i++) {
if ((selectedRows[i] > movedRow) && ((destRow > selectedRows[i]))) {
selectedRows[i] = selectedRows[i] - 1;
} else if ((selectedRows[i] < movedRow) &&
(destRow < selectedRows[i])) {
selectedRows[i] = selectedRows[i] + 1;
}
}
}
// Set current index.
// TODO If we moved down, pick the last selected row?
// int idxRow = destRow < firstSelRow