diff --git a/CHANGELOG.md b/CHANGELOG.md index 9672d81dc00..edb3900db59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -186,15 +186,17 @@ ### Controller Mappings +* Behringer DDM4000 & BCR2000: Fix exception in JS code [#12969](https://github.com/mixxxdj/mixxx/pull/12969) +* Denon DJ MC6000MK2: Fix mapping of filter knob/button [#13166](https://github.com/mixxxdj/mixxx/pull/13166) +* Denon DJ MC7000: Fix redundant argument and migrate to `hotcue_x_status` [#13113](https://github.com/mixxxdj/mixxx/pull/13113) [#13121](https://github.com/mixxxdj/mixxx/pull/13121) * Hercules Inpulse 200: Configure shift-browser knob to scroll the library (quick) [#12932](https://github.com/mixxxdj/mixxx/pull/12932) * Pioneer DDJ-FLX4: Add waveform zoom and other mapping improvements [#12896](https://github.com/mixxxdj/mixxx/pull/12896) [#12842](https://github.com/mixxxdj/mixxx/pull/12842) * Traktor Kontrol F1: Fixes for hid-parser and related script [#12876](https://github.com/mixxxdj/mixxx/pull/12876) +* Traktor S2 Mk1: fix warnings [#13145](https://github.com/mixxxdj/mixxx/pull/13145) * Traktor S3: Fix mapping crash on macOS [#12840](https://github.com/mixxxdj/mixxx/pull/12840) -* Behringer DDM4000 & BCR2000: Fix exception in JS code [#12969](https://github.com/mixxxdj/mixxx/pull/12969) -* Denon DJ MC7000: Fix redundant argument and migrate to `hotcue_x_status` [#13113](https://github.com/mixxxdj/mixxx/pull/13113) [#13121](https://github.com/mixxxdj/mixxx/pull/13121) -* Polish fx chain controls [#12805](https://github.com/mixxxdj/mixxx/pull/12805) +* Controller I/O table: sort action column by display string [#13039](https://github.com/mixxxdj/mixxx/pull/13039) ### Target Support @@ -212,9 +214,14 @@ * Deere: make sampler rows persist [#12928](https://github.com/mixxxdj/mixxx/pull/12928) * Tango: Remove unneeded waveform Singleton [#12938](https://github.com/mixxxdj/mixxx/pull/12938) -* Possible crash in customs skins using parallel waveforms [#13043](https://github.com/mixxxdj/mixxx/pull/13043) [#12580](https://github.com/mixxxdj/mixxx/issues/12580) +* Prevent possible crash in customs skins using parallel waveforms + [#13043](https://github.com/mixxxdj/mixxx/pull/13043) + [#12580](https://github.com/mixxxdj/mixxx/issues/12580) + [#13136](https://github.com/mixxxdj/mixxx/pull/13136) * Slider tooltip: consider orientation for up/down shortcut tooltips + add support for WKnobComposed [#13088](https://github.com/mixxxdj/mixxx/pull/13088) * Tooltips: update 'hotcue' with saved loop features [#12875](https://github.com/mixxxdj/mixxx/pull/12875) +* Animate long press latching of sync button [#12990](https://github.com/mixxxdj/mixxx/pull/12990) +* Polish fx chain controls [#12805](https://github.com/mixxxdj/mixxx/pull/12805) ### Library @@ -235,6 +242,17 @@ * Allow adding new directories while watched directories are missing [#12937](https://github.com/mixxxdj/mixxx/pull/12937) [#10481](https://github.com/mixxxdj/mixxx/issues/10481) +* Require a minimum movement before initiating the drag&drop of tracks + [#13135](https://github.com/mixxxdj/mixxx/pull/13135) + [#12902](https://github.com/mixxxdj/mixxx/issues/12902) +* iTunes/Serato/Traktor/Rhythmbox: Print error if library file could not be opened + [#13012](https://github.com/mixxxdj/mixxx/pull/13012) +* Playlists: improve table update after deleting (purging) track files + [#13127](https://github.com/mixxxdj/mixxx/pull/13127) +* Fix Color column width issue [#12852](https://github.com/mixxxdj/mixxx/pull/12852) +* Tracks: select track row when clicking the preview button (only when starting preview) + [#12791](https://github.com/mixxxdj/mixxx/pull/12791) +* Library track menu: show Hide action also in Playlist & Crates [#11901](https://github.com/mixxxdj/mixxx/pull/11901) ### Miscellaneous @@ -248,7 +266,18 @@ * AutoDJ: Fix button state after error message about playing deck 3/4 [#12976](https://github.com/mixxxdj/mixxx/pull/12976) [#12975](https://github.com/mixxxdj/mixxx/issues/12975) -* Tagfetcher: Cache fetched covers [#12301](https://github.com/mixxxdj/mixxx/pull/12301) [#11084](https://github.com/mixxxdj/mixxx/issues/11084) +* Tagfetcher: Cache fetched covers + [#12301](https://github.com/mixxxdj/mixxx/pull/12301) + [#11084](https://github.com/mixxxdj/mixxx/issues/11084) +* Avoid beats iterator being one off and DEBUG_ASSERT in Beats::iteratorFrom + [#13150](https://github.com/mixxxdj/mixxx/pull/13150) + [#13149](https://github.com/mixxxdj/mixxx/issues/13149) +* Show hint if resource path in CMakeCache.txt does not exist + [#12929](https://github.com/mixxxdj/mixxx/pull/12929) +* Always calculate the auto value for colorful console output [#13153](https://github.com/mixxxdj/mixxx/pull/13153) +* Fix FLAC recording on macOS and Windows + [#10880](https://github.com/mixxxdj/mixxx/issues/10880) + [#13154](https://github.com/mixxxdj/mixxx/pull/13154) ## [2.4.0](https://github.com/mixxxdj/mixxx/milestone/15?closed=1) (2024-02-16) diff --git a/packaging/debian/changelog b/packaging/debian/changelog index adc266efbd5..246c4e72fe8 100644 --- a/packaging/debian/changelog +++ b/packaging/debian/changelog @@ -1,3 +1,9 @@ +mixxx (2.4.0-1~focal) focal; urgency=medium + + * Build of 2.4.0 + + -- RJ Skerry-Ryan Thu, 15 Feb 2024 23:55:01 +0000 + mixxx (2.3.6-1~bionic) bionic; urgency=medium * Build of 2.3.6 diff --git a/res/controllers/Denon-MC6000MK2-scripts.js b/res/controllers/Denon-MC6000MK2-scripts.js index b9a7156bff1..6732229949e 100644 --- a/res/controllers/Denon-MC6000MK2-scripts.js +++ b/res/controllers/Denon-MC6000MK2-scripts.js @@ -612,7 +612,7 @@ DenonMC6000MK2.Sampler.prototype.connectControls = function() { DenonMC6000MK2.OldDeck = function(number, midiChannel) { this.number = number; this.group = "[Channel" + number + "]"; - this.filterGroup = "[QuickEffectRack1_" + this.group + "_Effect1]"; + this.filterGroup = "[QuickEffectRack1_" + this.group + "]"; this.midiChannel = midiChannel; this.jogTouchState = false; DenonMC6000MK2.oldDecksByGroup[this.group] = this; @@ -912,7 +912,7 @@ DenonMC6000MK2.OldDeck.prototype.spinJog = function(jogDelta) { DenonMC6000MK2.OldDeck.prototype.applyFilter = function() { var side = DenonMC6000MK2.getOldSideByGroup(this.group); engine.setValue(this.filterGroup, "enabled", side.filterEnabled); - engine.setParameter(this.filterGroup, "meta", side.filterParam); + engine.setParameter(this.filterGroup, "super1", side.filterParam); }; /* Loops */ diff --git a/res/linux/org.mixxx.Mixxx.metainfo.xml b/res/linux/org.mixxx.Mixxx.metainfo.xml index b3d50bc1e03..0c9005b751c 100644 --- a/res/linux/org.mixxx.Mixxx.metainfo.xml +++ b/res/linux/org.mixxx.Mixxx.metainfo.xml @@ -96,7 +96,7 @@ Do not edit it manually. --> - +

Features @@ -704,12 +704,26 @@ - +

Controller Mappings

    +
  • + Behringer DDM4000 & BCR2000: Fix exception in JS code + #12969 +
  • +
  • + Denon DJ MC6000MK2: Fix mapping of filter knob/button + #13166 +
  • +
  • + Denon DJ MC7000: Fix redundant argument and migrate to + hotcue_x_status + #13113 + #13121 +
  • Hercules Inpulse 200: Configure shift-browser knob to scroll the library (quick) #12932 @@ -724,22 +738,16 @@ #12876
  • - Traktor S3: Fix mapping crash on macOS - #12840 -
  • -
  • - Behringer DDM4000 & BCR2000: Fix exception in JS code - #12969 + Traktor S2 Mk1: fix warnings + #13145
  • - Denon DJ MC7000: Fix redundant argument and migrate to - hotcue_x_status - #13113 - #13121 + Traktor S3: Fix mapping crash on macOS + #12840
  • - Polish fx chain controls - #12805 + Controller I/O table: sort action column by display string + #13039

@@ -773,9 +781,10 @@ #12938

  • - Possible crash in customs skins using parallel waveforms + Prevent possible crash in customs skins using parallel waveforms #13043 #12580 + #13136
  • Slider tooltip: consider orientation for up/down shortcut tooltips + add support for WKnobComposed @@ -785,6 +794,14 @@ Tooltips: update 'hotcue' with saved loop features #12875
  • +
  • + Animate long press latching of sync button + #12990 +
  • +
  • + Polish fx chain controls + #12805 +
  • Library @@ -827,6 +844,31 @@ #12937 #10481 +

  • + Require a minimum movement before initiating the drag&drop of tracks + #13135 + #12902 +
  • +
  • + iTunes/Serato/Traktor/Rhythmbox: Print error if library file could not be opened + #13012 +
  • +
  • + Playlists: improve table update after deleting (purging) track files + #13127 +
  • +
  • + Fix Color column width issue + #12852 +
  • +
  • + Tracks: select track row when clicking the preview button (only when starting preview) + #12791 +
  • +
  • + Library track menu: show Hide action also in Playlist & Crates + #11901 +
  • Miscellaneous @@ -864,6 +906,24 @@ #12301 #11084 +

  • + Avoid beats iterator being one off and DEBUG_ASSERT in Beats::iteratorFrom + #13150 + #13149 +
  • +
  • + Show hint if resource path in CMakeCache.txt does not exist + #12929 +
  • +
  • + Always calculate the auto value for colorful console output + #13153 +
  • +
  • + Fix FLAC recording on macOS and Windows + #10880 + #13154 +
  • diff --git a/res/translations/mixxx_hy.qm b/res/translations/mixxx_hy.qm index f7f8d262191..ea0f9149c35 100644 Binary files a/res/translations/mixxx_hy.qm and b/res/translations/mixxx_hy.qm differ diff --git a/res/translations/mixxx_is.qm b/res/translations/mixxx_is.qm index dc0886e2b3d..ddee173366a 100644 Binary files a/res/translations/mixxx_is.qm and b/res/translations/mixxx_is.qm differ diff --git a/res/translations/mixxx_lb.qm b/res/translations/mixxx_lb.qm index 12d5bf1b0f5..39d21f0fc35 100644 Binary files a/res/translations/mixxx_lb.qm and b/res/translations/mixxx_lb.qm differ diff --git a/res/translations/mixxx_lt.qm b/res/translations/mixxx_lt.qm index 16842315fc0..0798dbf9449 100644 Binary files a/res/translations/mixxx_lt.qm and b/res/translations/mixxx_lt.qm differ diff --git a/res/translations/mixxx_lv.qm b/res/translations/mixxx_lv.qm index 84e33118af8..7a16cae1011 100644 Binary files a/res/translations/mixxx_lv.qm and b/res/translations/mixxx_lv.qm differ diff --git a/res/translations/mixxx_mi.qm b/res/translations/mixxx_mi.qm index c2e5c9449e4..ed9171e312d 100644 Binary files a/res/translations/mixxx_mi.qm and b/res/translations/mixxx_mi.qm differ diff --git a/res/translations/mixxx_mk.qm b/res/translations/mixxx_mk.qm index e15dcb31b71..c38fdcdb3de 100644 Binary files a/res/translations/mixxx_mk.qm and b/res/translations/mixxx_mk.qm differ diff --git a/res/translations/mixxx_ms.qm b/res/translations/mixxx_ms.qm index ff203ff2a59..9b088ff01fb 100644 Binary files a/res/translations/mixxx_ms.qm and b/res/translations/mixxx_ms.qm differ diff --git a/res/translations/mixxx_my.qm b/res/translations/mixxx_my.qm index 44d0864b193..a780a7b6eda 100644 Binary files a/res/translations/mixxx_my.qm and b/res/translations/mixxx_my.qm differ diff --git a/res/translations/mixxx_nn.qm b/res/translations/mixxx_nn.qm index 68522e81fd6..49986d69d6e 100644 Binary files a/res/translations/mixxx_nn.qm and b/res/translations/mixxx_nn.qm differ diff --git a/res/translations/mixxx_oc.qm b/res/translations/mixxx_oc.qm index d0c82cebb71..df091234edc 100644 Binary files a/res/translations/mixxx_oc.qm and b/res/translations/mixxx_oc.qm differ diff --git a/res/translations/mixxx_sq_AL.qm b/res/translations/mixxx_sq_AL.qm index ee848d32356..8a4bb607a6e 100644 Binary files a/res/translations/mixxx_sq_AL.qm and b/res/translations/mixxx_sq_AL.qm differ diff --git a/res/translations/mixxx_te.qm b/res/translations/mixxx_te.qm index 6f806296687..8437c89a210 100644 Binary files a/res/translations/mixxx_te.qm and b/res/translations/mixxx_te.qm differ diff --git a/res/translations/mixxx_uz.qm b/res/translations/mixxx_uz.qm index b94432dbde2..f8e52789fad 100644 Binary files a/res/translations/mixxx_uz.qm and b/res/translations/mixxx_uz.qm differ diff --git a/src/controllers/controllerinputmappingtablemodel.cpp b/src/controllers/controllerinputmappingtablemodel.cpp index 824202536ef..cafcbb3e1e5 100644 --- a/src/controllers/controllerinputmappingtablemodel.cpp +++ b/src/controllers/controllerinputmappingtablemodel.cpp @@ -221,12 +221,16 @@ QVariant ControllerInputMappingTableModel::data(const QModelIndex& index, return QVariant(mapping.options); } return QVariant::fromValue(mapping.options); - case MIDI_COLUMN_ACTION: - if (role == Qt::UserRole) { - // TODO(rryan): somehow get the delegate display text? - return QVariant(control->group + QStringLiteral(",") + control->item); + case MIDI_COLUMN_ACTION: { + if (role == Qt::UserRole) { // sort by displaystring + QStyledItemDelegate* del = getDelegateForIndex(index); + VERIFY_OR_DEBUG_ASSERT(del) { + return QString(); + } + return del->displayText(QVariant::fromValue(mapping.control), QLocale()); } - return QVariant::fromValue(control); + return QVariant::fromValue(mapping.control); + } case MIDI_COLUMN_COMMENT: return mapping.description; default: diff --git a/src/controllers/keyboard/keyboardeventfilter.cpp b/src/controllers/keyboard/keyboardeventfilter.cpp index 3a33f76f609..c11cdf4e3d6 100644 --- a/src/controllers/keyboard/keyboardeventfilter.cpp +++ b/src/controllers/keyboard/keyboardeventfilter.cpp @@ -118,41 +118,42 @@ bool KeyboardEventFilter::eventFilter(QObject*, QEvent* e) { return false; } +// static QKeySequence KeyboardEventFilter::getKeySeq(QKeyEvent* e) { - QString modseq; - QKeySequence k; - - // TODO(XXX) check if we may simply return QKeySequence(e->modifiers()+e->key()) - - if (e->modifiers() & Qt::ShiftModifier) { - modseq += "Shift+"; - } - - if (e->modifiers() & Qt::ControlModifier) { - modseq += "Ctrl+"; - } - - if (e->modifiers() & Qt::AltModifier) { - modseq += "Alt+"; - } - - if (e->modifiers() & Qt::MetaModifier) { - modseq += "Meta+"; - } - if (e->key() >= 0x01000020 && e->key() <= 0x01000023) { - // Do not act on Modifier only - // avoid returning "khmer vowel sign ie (U+17C0)" - return k; + // Do not act on Modifier only, avoid returning "khmer vowel sign ie (U+17C0)" + return {}; } - QString keyseq = QKeySequence(e->key()).toString(); - k = QKeySequence(modseq + keyseq); - if (CmdlineArgs::Instance().getDeveloper()) { - qDebug() << "keyboard press: " << k.toString(); + QString modseq; + QKeySequence k; + if (e->modifiers() & Qt::ShiftModifier) { + modseq += "Shift+"; + } + if (e->modifiers() & Qt::ControlModifier) { + modseq += "Ctrl+"; + } + if (e->modifiers() & Qt::AltModifier) { + modseq += "Alt+"; + } + if (e->modifiers() & Qt::MetaModifier) { + modseq += "Meta+"; + } + QString keyseq = QKeySequence(e->key()).toString(); + k = QKeySequence(modseq + keyseq); + if (e->type() == QEvent::KeyPress) { + qDebug() << "keyboard press: " << k.toString(); + } else if (e->type() == QEvent::KeyRelease) { + qDebug() << "keyboard release: " << k.toString(); + } } - return k; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return QKeySequence(e->modifiers() | e->key()); +#else + return QKeySequence(e->modifiers() + e->key()); +#endif } void KeyboardEventFilter::setKeyboardConfig(ConfigObject* pKbdConfigObject) { diff --git a/src/controllers/keyboard/keyboardeventfilter.h b/src/controllers/keyboard/keyboardeventfilter.h index 4e35cc75d3d..ccb3fdcff8f 100644 --- a/src/controllers/keyboard/keyboardeventfilter.h +++ b/src/controllers/keyboard/keyboardeventfilter.h @@ -25,6 +25,9 @@ class KeyboardEventFilter : public QObject { void setKeyboardConfig(ConfigObject *pKbdConfigObject); ConfigObject* getKeyboardConfig(); + // Returns a valid QString with modifier keys from a QKeyEvent + static QKeySequence getKeySeq(QKeyEvent* e); + private: struct KeyDownInformation { KeyDownInformation(int keyId, int modifiers, ControlObject* pControl) @@ -38,9 +41,6 @@ class KeyboardEventFilter : public QObject { ControlObject* pControl; }; - // Returns a valid QString with modifier keys from a QKeyEvent - QKeySequence getKeySeq(QKeyEvent *e); - // Run through list of active keys to see if the pressed key is already active // and is not a control that repeats when held. bool shouldSkipHeldKey(int keyId) { @@ -51,7 +51,6 @@ class KeyboardEventFilter : public QObject { return keyDownInfo.keyId == keyId && !keyDownInfo.pControl->getKbdRepeatable(); }); } - // List containing keys which is currently pressed QList m_qActiveKeyList; // Pointer to keyboard config object diff --git a/src/library/basetracktablemodel.cpp b/src/library/basetracktablemodel.cpp index fc2c0721f2d..d6277a09913 100644 --- a/src/library/basetracktablemodel.cpp +++ b/src/library/basetracktablemodel.cpp @@ -534,6 +534,16 @@ QVariant BaseTrackTableModel::data( } } + // Return the preferred (default) width of the Color column. + // This works around inconsistencies when the width is determined by + // color values. See https://github.com/mixxxdj/mixxx/issues/12850 + if (role == Qt::SizeHintRole) { + const auto field = mapColumn(index.column()); + if (field == ColumnCache::COLUMN_LIBRARYTABLE_COLOR) { + return QSize(defaultColumnWidth() / 2, 0); + } + } + // Only retrieve a value for supported roles if (role != Qt::DisplayRole && role != Qt::EditRole && diff --git a/src/library/dao/playlistdao.cpp b/src/library/dao/playlistdao.cpp index ede2f4eb569..cbdea89ff4e 100644 --- a/src/library/dao/playlistdao.cpp +++ b/src/library/dao/playlistdao.cpp @@ -399,7 +399,8 @@ bool PlaylistDAO::removeTracksFromPlaylist(int playlistId, int startIndex) { return false; } transaction.commit(); - emit tracksChanged(QSet{playlistId}); + emit playlistContentChanged(QSet{playlistId}); + emit tracksRemoved(QSet{playlistId}); return true; } @@ -441,7 +442,8 @@ bool PlaylistDAO::appendTracksToPlaylist(const QList& trackIds, const i // TODO(XXX) don't emit if the track didn't add successfully. emit trackAdded(playlistId, trackId, insertPosition++); } - emit tracksChanged(QSet{playlistId}); + emit tracksAdded(QSet{playlistId}); + emit playlistContentChanged(QSet{playlistId}); return true; } @@ -555,7 +557,7 @@ bool PlaylistDAO::isHidden(const int playlistId) const { void PlaylistDAO::removeHiddenTracks(const int playlistId) { ScopedTransaction transaction(m_database); - // This query deletes all tracks marked as deleted and all + // This query deletes all tracks marked as hidden and all // phantom track_ids with no match in the library table QSqlQuery query(m_database); query.prepare(QStringLiteral( @@ -580,14 +582,16 @@ void PlaylistDAO::removeHiddenTracks(const int playlistId) { } transaction.commit(); - emit tracksChanged(QSet{playlistId}); + emit playlistContentChanged(QSet{playlistId}); + emit tracksRemoved(QSet{playlistId}); } void PlaylistDAO::removeTracksFromPlaylistById(int playlistId, TrackId trackId) { ScopedTransaction transaction(m_database); removeTracksFromPlaylistByIdInner(playlistId, trackId); transaction.commit(); - emit tracksChanged(QSet{playlistId}); + emit playlistContentChanged(QSet{playlistId}); + emit tracksRemoved(QSet{playlistId}); } void PlaylistDAO::removeTracksFromPlaylistByIdInner(int playlistId, TrackId trackId) { @@ -616,7 +620,8 @@ void PlaylistDAO::removeTrackFromPlaylist(int playlistId, int position) { ScopedTransaction transaction(m_database); removeTracksFromPlaylistInner(playlistId, position); transaction.commit(); - emit tracksChanged(QSet{playlistId}); + emit playlistContentChanged(QSet{playlistId}); + emit tracksRemoved(QSet{playlistId}); } void PlaylistDAO::removeTracksFromPlaylist(int playlistId, const QList& positions) { @@ -631,7 +636,8 @@ void PlaylistDAO::removeTracksFromPlaylist(int playlistId, const QList& pos removeTracksFromPlaylistInner(playlistId, position); } transaction.commit(); - emit tracksChanged(QSet{playlistId}); + emit playlistContentChanged(QSet{playlistId}); + emit tracksRemoved(QSet{playlistId}); } void PlaylistDAO::removeTracksFromPlaylistInner(int playlistId, int position) { @@ -726,7 +732,8 @@ bool PlaylistDAO::insertTrackIntoPlaylist(TrackId trackId, const int playlistId, m_playlistsTrackIsIn.insert(trackId, playlistId); emit trackAdded(playlistId, trackId, position); - emit tracksChanged(QSet{playlistId}); + emit tracksAdded(QSet{playlistId}); + emit playlistContentChanged(QSet{playlistId}); return true; } @@ -737,7 +744,7 @@ int PlaylistDAO::insertTracksIntoPlaylist(const QList& trackIds, return 0; } - int tracksAdded = 0; + int numTracksAdded = 0; ScopedTransaction transaction(m_database); int max_position = getMaxPosition(playlistId) + 1; @@ -781,7 +788,7 @@ int PlaylistDAO::insertTracksIntoPlaylist(const QList& trackIds, // Increment the insert position for the track. ++insertPositon; - ++tracksAdded; + ++numTracksAdded; } transaction.commit(); @@ -792,8 +799,8 @@ int PlaylistDAO::insertTracksIntoPlaylist(const QList& trackIds, // TODO(XXX) The position is wrong if any track failed to insert. emit trackAdded(playlistId, trackId, insertPositon++); } - emit tracksChanged(QSet{playlistId}); - return tracksAdded; + emit tracksAdded(QSet{playlistId}); + return numTracksAdded; } void PlaylistDAO::clearAutoDJQueue() { @@ -926,7 +933,8 @@ bool PlaylistDAO::copyPlaylistTracks(const int sourcePlaylistID, const int targe m_playlistsTrackIsIn.insert(copiedTrackId, targetPlaylistID); emit trackAdded(targetPlaylistID, copiedTrackId, copiedPosition); } - emit tracksChanged(QSet{targetPlaylistID}); + emit tracksAdded(QSet{targetPlaylistID}); + emit playlistContentChanged(QSet{targetPlaylistID}); return true; } @@ -950,7 +958,7 @@ int PlaylistDAO::getMaxPosition(const int playlistId) const { return position; } -void PlaylistDAO::removeTracksFromPlaylists(const QList& trackIds) { +void PlaylistDAO::removeTracksFromPlaylists(const QList& trackIds, bool purged) { // copy the hash, because there is no guarantee that "it" is valid after remove QMultiHash playlistsTrackIsInCopy = m_playlistsTrackIsIn; QSet playlistIds; @@ -973,7 +981,17 @@ void PlaylistDAO::removeTracksFromPlaylists(const QList& trackIds) { } transaction.commit(); - emit tracksChanged(playlistIds); + // update the sidebar + emit playlistContentChanged(playlistIds); + // If this is called by TrackCollection::purgeTracks() it will call + // TrackDAO::afterPurgingTracks() afterwards which will enforce a model update + // (select()), so we don't need to signal PlaylistTableModel to select(), + // Also, this double select() can cause issues in BaseTrackCache. + // See https://github.com/mixxxdj/mixxx/issues/13111 + if (purged) { + return; + } + emit tracksRemoved(playlistIds); } int PlaylistDAO::tracksInPlaylist(const int playlistId) const { @@ -1059,7 +1077,7 @@ void PlaylistDAO::moveTrack(const int playlistId, const int oldPosition, const i qDebug() << query.lastError(); } - emit tracksChanged(QSet{playlistId}); + emit tracksMoved(QSet{playlistId}); } void PlaylistDAO::searchForDuplicateTrack(const int fromPosition, @@ -1265,7 +1283,7 @@ void PlaylistDAO::shuffleTracks(const int playlistId, } transaction.commit(); - emit tracksChanged(QSet{playlistId}); + emit tracksMoved(QSet{playlistId}); } bool PlaylistDAO::isTrackInPlaylist(TrackId trackId, const int playlistId) const { diff --git a/src/library/dao/playlistdao.h b/src/library/dao/playlistdao.h index 3eb6def5ec2..eba10df97ea 100644 --- a/src/library/dao/playlistdao.h +++ b/src/library/dao/playlistdao.h @@ -96,7 +96,7 @@ class PlaylistDAO : public QObject, public virtual DAO { // Returns the maximum position of the given playlist int getMaxPosition(const int playlistId) const; // Remove a track from all playlists - void removeTracksFromPlaylists(const QList& trackIds); + void removeTracksFromPlaylists(const QList& trackIds, bool purged = false); // removes all hidden and purged Tracks from the playlist void removeHiddenTracks(const int playlistId); // Remove a track from a playlist @@ -141,7 +141,12 @@ class PlaylistDAO : public QObject, public virtual DAO { void lockChanged(const QSet& playlistIds); void trackAdded(int playlistId, TrackId trackId, int position); void trackRemoved(int playlistId, TrackId trackId, int position); - void tracksChanged(const QSet& playlistIds); // added/removed/reordered + // added / removed / un/locked. Triggers playlist features to update the sidebar + void playlistContentChanged(const QSet& playlistIds); + // Separate signals for PlaylistTableModel + void tracksAdded(const QSet& playlistIds); + void tracksMoved(const QSet& playlistIds); + void tracksRemoved(const QSet& playlistIds); void tracksRemovedFromPlayedHistory(const QSet& playedTrackIds); private: diff --git a/src/library/itunes/itunesxmlimporter.cpp b/src/library/itunes/itunesxmlimporter.cpp index db11ad25c37..8a2ccfebb1b 100644 --- a/src/library/itunes/itunesxmlimporter.cpp +++ b/src/library/itunes/itunesxmlimporter.cpp @@ -66,7 +66,8 @@ ITunesImport ITunesXMLImporter::importLibrary() { bool isMusicFolderLocatedAfterTracks = false; if (!m_xmlFile.open(QIODevice::ReadOnly)) { - qWarning() << "Could not open iTunes music collection XML at " << m_xmlFilePath; + qWarning() << "Could not open iTunes music collection XML at " << m_xmlFilePath + << ":" << m_xmlFile.errorString(); return iTunesImport; } diff --git a/src/library/library.cpp b/src/library/library.cpp index d1f73fee806..eaef0121bdd 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -706,6 +706,18 @@ bool Library::isTrackIdInCurrentLibraryView(const TrackId& trackId) { } } +void Library::slotSaveCurrentViewState() const { + if (m_pLibraryWidget) { + return m_pLibraryWidget->saveCurrentViewState(); + } +} + +void Library::slotRestoreCurrentViewState() const { + if (m_pLibraryWidget) { + return m_pLibraryWidget->restoreCurrentViewState(); + } +} + LibraryTableModel* Library::trackTableModel() const { VERIFY_OR_DEBUG_ASSERT(m_pMixxxLibraryFeature) { return nullptr; diff --git a/src/library/library.h b/src/library/library.h index 3cd08cc23e5..443c48a6f5f 100644 --- a/src/library/library.h +++ b/src/library/library.h @@ -115,6 +115,8 @@ class Library: public QObject { void slotRequestRemoveDir(const QString& directory, LibraryRemovalType removalType); void slotRequestRelocateDir(const QString& previousDirectory, const QString& newDirectory); void onSkinLoadFinished(); + void slotSaveCurrentViewState() const; + void slotRestoreCurrentViewState() const; signals: void showTrackModel(QAbstractItemModel* model, bool restoreState = true); diff --git a/src/library/playlisttablemodel.cpp b/src/library/playlisttablemodel.cpp index 56e512d6053..069a460d903 100644 --- a/src/library/playlisttablemodel.cpp +++ b/src/library/playlisttablemodel.cpp @@ -21,7 +21,15 @@ PlaylistTableModel::PlaylistTableModel(QObject* parent, m_iPlaylistId(kInvalidPlaylistId), m_keepHiddenTracks(keepHiddenTracks) { connect(&m_pTrackCollectionManager->internalCollection()->getPlaylistDAO(), - &PlaylistDAO::tracksChanged, + &PlaylistDAO::tracksAdded, + this, + &PlaylistTableModel::playlistsChanged); + connect(&m_pTrackCollectionManager->internalCollection()->getPlaylistDAO(), + &PlaylistDAO::tracksMoved, + this, + &PlaylistTableModel::playlistsChanged); + connect(&m_pTrackCollectionManager->internalCollection()->getPlaylistDAO(), + &PlaylistDAO::tracksRemoved, this, &PlaylistTableModel::playlistsChanged); } @@ -363,6 +371,8 @@ TrackModel::Capabilities PlaylistTableModel::getCapabilities() const { Capability::LoadToSampler | Capability::LoadToPreviewDeck | Capability::ResetPlayed | + Capability::RemoveFromDisk | + Capability::Hide | Capability::Analyze; if (m_iPlaylistId != @@ -377,8 +387,9 @@ TrackModel::Capabilities PlaylistTableModel::getCapabilities() const { if (m_pTrackCollectionManager->internalCollection() ->getPlaylistDAO() .getHiddenType(m_iPlaylistId) == PlaylistDAO::PLHT_SET_LOG) { - // Disable track reordering and adding tracks via drag'n'drop for history playlists - caps &= ~(Capability::ReceiveDrops | Capability::Reorder); + // Disallow reordering and hiding tracks, as well as adding tracks via + // drag'n'drop for history playlists + caps &= ~(Capability::ReceiveDrops | Capability::Reorder | Capability::Hide); } bool locked = m_pTrackCollectionManager->internalCollection()->getPlaylistDAO().isPlaylistLocked(m_iPlaylistId); if (locked) { diff --git a/src/library/rhythmbox/rhythmboxfeature.cpp b/src/library/rhythmbox/rhythmboxfeature.cpp index f324d8d02fc..9a38477e2b3 100644 --- a/src/library/rhythmbox/rhythmboxfeature.cpp +++ b/src/library/rhythmbox/rhythmboxfeature.cpp @@ -164,6 +164,7 @@ TreeItem* RhythmboxFeature::importMusicCollection() { mixxx::FileInfo fileInfo(db); if (!Sandbox::askForAccess(&fileInfo) || !db.open(QIODevice::ReadOnly)) { + qWarning() << "Could not open Rhythmbox db at" << db.fileName() << db.errorString(); return nullptr; } diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 6f1803647ee..3ebc511bb1c 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -348,7 +348,8 @@ QString parseCrate( if (!Sandbox::askForAccess(&fileInfo) || !crateFile.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file " << crateFilePath - << " for reading."; + << " for reading:" + << crateFile.errorString(); return QString(); } diff --git a/src/library/tabledelegates/previewbuttondelegate.cpp b/src/library/tabledelegates/previewbuttondelegate.cpp index 110b84d69fa..87c897ec95f 100644 --- a/src/library/tabledelegates/previewbuttondelegate.cpp +++ b/src/library/tabledelegates/previewbuttondelegate.cpp @@ -49,8 +49,7 @@ PreviewButtonDelegate::PreviewButtonDelegate( m_column(column), m_pPreviewDeckPlay(make_parented( kPreviewDeckGroup, QStringLiteral("play"), this)), - m_pCueGotoAndPlay(make_parented( - kPreviewDeckGroup, QStringLiteral("cue_gotoandplay"), this)), + m_pCueGotoAndPlay(kPreviewDeckGroup, QStringLiteral("cue_gotoandplay")), m_pButton(make_parented(parent)) { DEBUG_ASSERT(m_column >= 0); @@ -217,16 +216,24 @@ void PreviewButtonDelegate::buttonClicked() { TrackPointer pOldTrack = PlayerInfo::instance().getTrackInfo(kPreviewDeckGroup); + bool startedPlaying = false; TrackPointer pTrack = pTrackModel->getTrack(m_currentEditedCellIndex); if (pTrack && pTrack != pOldTrack) { + // Load to preview deck and start playing emit loadTrackToPlayer(pTrack, kPreviewDeckGroup, true); + startedPlaying = true; } else if (pTrack == pOldTrack && !isPreviewDeckPlaying()) { - // Since the Preview deck might be hidden - // Starting at cue is a predictable behavior - m_pCueGotoAndPlay->set(1.0); + // Since the Preview deck might be hidden, starting at the main cue + // is a predictable behavior. + m_pCueGotoAndPlay.set(1.0); + startedPlaying = true; } else { m_pPreviewDeckPlay->set(0.0); } + // If we start previewing also select the track (the table view didn't receive the click) + if (startedPlaying) { + m_pTableView->selectRow(m_currentEditedCellIndex.row()); + } } void PreviewButtonDelegate::previewDeckPlayChanged(double v) { diff --git a/src/library/tabledelegates/previewbuttondelegate.h b/src/library/tabledelegates/previewbuttondelegate.h index 6bb37e514f9..a42b206bc12 100644 --- a/src/library/tabledelegates/previewbuttondelegate.h +++ b/src/library/tabledelegates/previewbuttondelegate.h @@ -2,6 +2,7 @@ #include +#include "control/pollingcontrolproxy.h" #include "library/tabledelegates/tableitemdelegate.h" #include "track/track_decl.h" #include "util/parented_ptr.h" @@ -31,9 +32,12 @@ class PreviewButtonDelegate : public TableItemDelegate { const QStyleOptionViewItem& option, const QModelIndex& index) const override; + // Apparently this no-op override is required to trigger a paint + // event after row has been painted with the 'selected' style. (Qt 5) void setEditorData( QWidget* editor, const QModelIndex& index) const override; + // Seems this is not required void setModelData( QWidget* editor, QAbstractItemModel* model, @@ -74,7 +78,7 @@ class PreviewButtonDelegate : public TableItemDelegate { const int m_column; const parented_ptr m_pPreviewDeckPlay; - const parented_ptr m_pCueGotoAndPlay; + PollingControlProxy m_pCueGotoAndPlay; const parented_ptr m_pButton; diff --git a/src/library/trackcollection.cpp b/src/library/trackcollection.cpp index 8bd9b511b0f..3991c6711b1 100644 --- a/src/library/trackcollection.cpp +++ b/src/library/trackcollection.cpp @@ -373,7 +373,7 @@ bool TrackCollection::purgeTracks( } // TODO(XXX): Move reversible actions inside transaction m_cueDao.deleteCuesForTracks(trackIds); - m_playlistDao.removeTracksFromPlaylists(trackIds); + m_playlistDao.removeTracksFromPlaylists(trackIds, true); m_analysisDao.deleteAnalyses(trackIds); // Post-processing diff --git a/src/library/trackset/baseplaylistfeature.cpp b/src/library/trackset/baseplaylistfeature.cpp index 4f230f0ad54..9d48a5e9f7e 100644 --- a/src/library/trackset/baseplaylistfeature.cpp +++ b/src/library/trackset/baseplaylistfeature.cpp @@ -158,7 +158,7 @@ void BasePlaylistFeature::connectPlaylistDAO() { this, &BasePlaylistFeature::slotPlaylistTableChanged); connect(&m_playlistDao, - &PlaylistDAO::tracksChanged, + &PlaylistDAO::playlistContentChanged, this, &BasePlaylistFeature::slotPlaylistContentOrLockChanged); connect(&m_playlistDao, diff --git a/src/library/trackset/crate/cratetablemodel.cpp b/src/library/trackset/crate/cratetablemodel.cpp index d574f90eb35..ebc16b629f9 100644 --- a/src/library/trackset/crate/cratetablemodel.cpp +++ b/src/library/trackset/crate/cratetablemodel.cpp @@ -131,6 +131,7 @@ TrackModel::Capabilities CrateTableModel::getCapabilities() const { Capability::LoadToPreviewDeck | Capability::RemoveCrate | Capability::ResetPlayed | + Capability::Hide | Capability::RemoveFromDisk | Capability::Analyze; diff --git a/src/library/traktor/traktorfeature.cpp b/src/library/traktor/traktorfeature.cpp index d6773cf5fc3..0e9da33be31 100644 --- a/src/library/traktor/traktorfeature.cpp +++ b/src/library/traktor/traktorfeature.cpp @@ -224,7 +224,7 @@ TreeItem* TraktorFeature::importLibrary(const QString& file) { mixxx::FileInfo fileInfo(file); QFile traktor_file(file); if (!Sandbox::askForAccess(&fileInfo) || !traktor_file.open(QIODevice::ReadOnly)) { - qDebug() << "Cannot open Traktor music collection"; + qDebug() << "Cannot open Traktor music collection: " << traktor_file.errorString(); return nullptr; } QXmlStreamReader xml(&traktor_file); diff --git a/src/main.cpp b/src/main.cpp index 9aa709244e8..60cb824a7ef 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -50,10 +50,10 @@ const QString kScaleFactorKey = QStringLiteral("ScaleFactor"); constexpr int kPixmapCacheLimitAt100PercentZoom = 32 * 1024; // 32 MByte int runMixxx(MixxxApplication* pApp, const CmdlineArgs& args) { - const auto pCoreServices = std::make_shared(args, pApp); - CmdlineArgs::Instance().parseForUserFeedback(); + const auto pCoreServices = std::make_shared(args, pApp); + int exitCode; #ifdef MIXXX_USE_QML if (args.isQml()) { diff --git a/src/preferences/configobject.cpp b/src/preferences/configobject.cpp index 92b596b09ab..81709fada49 100644 --- a/src/preferences/configobject.cpp +++ b/src/preferences/configobject.cpp @@ -57,7 +57,13 @@ QString computeResourcePathImpl() { } line = in.readLine(); } - DEBUG_ASSERT(QDir(qResourcePath).exists()); + if (!QDir(qResourcePath).exists()) { + reportCriticalErrorAndQuit( + "Resource path listed in " + kCMakeCacheFile + + " does not exist. Did you move the build directory? " + "Hint: Set an alternative resource path with " + "'--resource-path '."); + } } #if defined(__UNIX__) else if (mixxxDir.cd(QStringLiteral("../share/mixxx"))) { diff --git a/src/skin/legacy/legacyskinparser.cpp b/src/skin/legacy/legacyskinparser.cpp index b76bbf2567c..9bc4f0fcd5d 100644 --- a/src/skin/legacy/legacyskinparser.cpp +++ b/src/skin/legacy/legacyskinparser.cpp @@ -1109,6 +1109,20 @@ QWidget* LegacySkinParser::parseTrackProperty(const QDomElement& node) { } } + // Relay for the label's WTrackMenu (which is created only on demand): + // Emitted before/after deleting a track file via that menu + // IF that track is in the current view. + connect(pTrackProperty, + &WTrackProperty::saveCurrentViewState, + m_pLibrary, + &Library::slotSaveCurrentViewState, + Qt::DirectConnection); + connect(pTrackProperty, + &WTrackProperty::restoreCurrentViewState, + m_pLibrary, + &Library::slotRestoreCurrentViewState, + Qt::DirectConnection); + connect(pPlayer, &BaseTrackPlayer::newTrackLoaded, pTrackProperty, diff --git a/src/util/cmdlineargs.cpp b/src/util/cmdlineargs.cpp index 8fd7b237768..9636b1028fd 100644 --- a/src/util/cmdlineargs.cpp +++ b/src/util/cmdlineargs.cpp @@ -18,6 +18,28 @@ #include "sources/soundsourceproxy.h" #include "util/assert.h" +namespace { + +bool calcUseColorsAuto() { + // see https://no-color.org/ + if (QProcessEnvironment::systemEnvironment().contains(QLatin1String("NO_COLOR"))) { + return false; + } else { +#ifndef __WINDOWS__ + if (isatty(fileno(stderr))) { + return true; + } +#else + if (_isatty(_fileno(stderr))) { + return true; + } +#endif + } + return false; +} + +} // namespace + CmdlineArgs::CmdlineArgs() : m_startInFullscreen(false), // Initialize vars m_startAutoDJ(false), @@ -33,7 +55,7 @@ CmdlineArgs::CmdlineArgs() m_debugAssertBreak(false), m_settingsPathSet(false), m_scaleFactor(1.0), - m_useColors(false), + m_useColors(calcUseColorsAuto()), m_parseForUserFeedbackRequired(false), m_logLevel(mixxx::kLogLevelDefault), m_logFlushLevel(mixxx::kLogFlushLevelDefault), @@ -429,26 +451,11 @@ bool CmdlineArgs::parse(const QStringList& arguments, CmdlineArgs::ParseMode mod } // set colors - if (parser.value(color).compare(QLatin1String("auto"), Qt::CaseInsensitive) == 0) { - // see https://no-color.org/ - if (QProcessEnvironment::systemEnvironment().contains(QLatin1String("NO_COLOR"))) { - m_useColors = false; - } else { -#ifndef __WINDOWS__ - if (isatty(fileno(stderr))) { - m_useColors = true; - } -#else - if (_isatty(_fileno(stderr))) { - m_useColors = true; - } -#endif - } - } else if (parser.value(color).compare(QLatin1String("always"), Qt::CaseInsensitive) == 0) { + if (parser.value(color).compare(QLatin1String("always"), Qt::CaseInsensitive) == 0) { m_useColors = true; } else if (parser.value(color).compare(QLatin1String("never"), Qt::CaseInsensitive) == 0) { m_useColors = false; - } else { + } else if (parser.value(color).compare(QLatin1String("auto"), Qt::CaseInsensitive) != 0) { fputs("Unknown argument for for color.\n", stdout); } diff --git a/src/util/opengltexture2d.h b/src/util/opengltexture2d.h index bcf3ccbd9e5..7e07f1fda7c 100644 --- a/src/util/opengltexture2d.h +++ b/src/util/opengltexture2d.h @@ -2,8 +2,10 @@ #include #include +#include class Paintable; +class QImage; /// This is an QOpenGLTexture, with additional methods to set the texture data, /// and default settings for 2D painting with lienar filtering and wrap mode. diff --git a/src/widget/wlibrary.cpp b/src/widget/wlibrary.cpp index b408c625c74..291a63b27b1 100644 --- a/src/widget/wlibrary.cpp +++ b/src/widget/wlibrary.cpp @@ -102,27 +102,29 @@ LibraryView* WLibrary::getActiveView() const { return dynamic_cast(currentWidget()); } -bool WLibrary::isTrackInCurrentView(const TrackId& trackId) { - //qDebug() << "WLibrary::isTrackInCurrentView" << trackId; - VERIFY_OR_DEBUG_ASSERT(trackId.isValid()) { - return false; - } +WTrackTableView* WLibrary::getCurrentTrackTableView() const { QWidget* pCurrent = currentWidget(); WTrackTableView* pTracksView = qobject_cast(pCurrent); if (!pTracksView) { // This view is no tracks view, but maybe a special tracks view with a - // controls row (AutoDJ, Recording)? - //qDebug() << " view is no tracks view. look for tracks view child"; + // controls row (DlgAutoDJ, DlgRecording)? + // qDebug() << " view is no tracks view. look for tracks view child"; pTracksView = pCurrent->findChild(); } - if (pTracksView) { - //qDebug() << " tracks view found"; - return pTracksView->isTrackInCurrentView(trackId); - } else { - // No tracks view, this is probably a root view WLibraryTextBrowser - //qDebug() << " no tracks view found"; + return pTracksView; // might be nullptr +} + +bool WLibrary::isTrackInCurrentView(const TrackId& trackId) { + // qDebug() << "WLibrary::isTrackInCurrentView" << trackId; + VERIFY_OR_DEBUG_ASSERT(trackId.isValid()) { + return false; + } + WTrackTableView* pTracksView = getCurrentTrackTableView(); + if (!pTracksView) { return false; } + + return pTracksView->isTrackInCurrentView(trackId); } void WLibrary::slotSelectTrackInActiveTrackView(const TrackId& trackId) { @@ -130,19 +132,27 @@ void WLibrary::slotSelectTrackInActiveTrackView(const TrackId& trackId) { if (!trackId.isValid()) { return; } + WTrackTableView* pTracksView = getCurrentTrackTableView(); + if (!pTracksView) { + return; + } + pTracksView->slotSelectTrack(trackId); +} - QWidget* pCurrent = currentWidget(); - WTrackTableView* pTracksView = qobject_cast(pCurrent); +void WLibrary::saveCurrentViewState() const { + WTrackTableView* pTracksView = getCurrentTrackTableView(); if (!pTracksView) { - //qDebug() << " view is no tracks view. look for tracks view child"; - pTracksView = pCurrent->findChild(); + return; } - if (pTracksView) { - //qDebug() << " tracks view found"; - pTracksView->slotSelectTrack(trackId); - } else { - //qDebug() << " no tracks view found"; + pTracksView->slotSaveCurrentViewState(); +} + +void WLibrary::restoreCurrentViewState() const { + WTrackTableView* pTracksView = getCurrentTrackTableView(); + if (!pTracksView) { + return; } + pTracksView->slotRestoreCurrentViewState(); } bool WLibrary::event(QEvent* pEvent) { diff --git a/src/widget/wlibrary.h b/src/widget/wlibrary.h index 0b99e6d90a7..a017e9d823d 100644 --- a/src/widget/wlibrary.h +++ b/src/widget/wlibrary.h @@ -11,6 +11,7 @@ #include "widget/wbasewidget.h" class LibraryView; +class WTrackTableView; class TrackId; class QDomNode; class SkinContext; @@ -31,13 +32,16 @@ class WLibrary : public QStackedWidget, public WBaseWidget { bool registerView(const QString& name, QWidget* view); LibraryView* getActiveView() const; - + WTrackTableView* getCurrentTrackTableView() const; // This returns true if the current view is or has a WTracksTableView and // contains trackId, otherwise false. // This is primarily used to disable the "Select track in library" track menu action // to avoid unintended behaviour if the current view has no tracks table. bool isTrackInCurrentView(const TrackId& trackId); + void saveCurrentViewState() const; + void restoreCurrentViewState() const; + // Alpha value for row color background static constexpr double kDefaultTrackTableBackgroundColorOpacity = 0.125; // 12.5% opacity static constexpr double kMinTrackTableBackgroundColorOpacity = 0.0; // 0% opacity diff --git a/src/widget/wtrackmenu.cpp b/src/widget/wtrackmenu.cpp index a7e11bffb33..57c5d22a751 100644 --- a/src/widget/wtrackmenu.cpp +++ b/src/widget/wtrackmenu.cpp @@ -313,7 +313,10 @@ void WTrackMenu::createActions() { m_pHideAct = new QAction(tr("Hide from Library"), this); // This is just for having the shortcut displayed next to the action in the menu. // The actual keypress is handled in WTrackTableView::keyPressEvent(). - m_pHideAct->setShortcut(hideRemoveKeySequence); + // Note: don't show the hotkey for more than one action + if (!featureIsEnabled(Feature::Remove)) { + m_pHideAct->setShortcut(hideRemoveKeySequence); + } connect(m_pHideAct, &QAction::triggered, this, &WTrackMenu::slotHide); m_pUnhideAct = new QAction(tr("Unhide from Library"), this); @@ -1056,9 +1059,9 @@ void WTrackMenu::updateMenus() { if (featureIsEnabled(Feature::HideUnhidePurge)) { bool locked = m_pTrackModel->hasCapabilities(TrackModel::Capability::Locked); - if (m_pTrackModel->hasCapabilities(TrackModel::Capability::Hide)) { - m_pHideAct->setEnabled(!locked); - } + // Note: Hide action is enabled regardless the locked state. + // Like in Tracks, in locked playlists A confirmation dialog pops up: + // "Hiding track ... will remove it from the following playlists: ..." if (m_pTrackModel->hasCapabilities(TrackModel::Capability::Unhide)) { m_pUnhideAct->setEnabled(!locked); } @@ -2274,10 +2277,21 @@ void WTrackMenu::slotRemoveFromDisk() { // If the operation was initiated from a deck's track menu // we'll first stop the deck and eject the track. // TODO(ronso0) Consider querying PlayerManager if any of the tracks is loaded - // into a (playing?) deck? + // into another (playing?) deck? + // Also (rare situation) the track we work on might have been replaced (in the deck) + // by another one in the mean time (Auto DJ) and we would unnecessarily stop & eject. + // Ideally, there would be a PlayerManager instance that does + // stopAndEjectAllPlayersWithTrackLoaded(TrackPointer / TrackId) + bool restoreViewState = false; if (m_pTrack) { ControlObject::set(ConfigKey(m_deckGroup, "stop"), 1.0); ControlObject::set(ConfigKey(m_deckGroup, "eject"), 1.0); + // Try to keep a usable index for navigation if the track is in the + // current track view. + if (m_pLibrary->isTrackIdInCurrentLibraryView(m_pTrack->getId())) { + restoreViewState = true; + emit saveCurrentViewState(); + } } // Set up and initiate the track batch operation @@ -2346,8 +2360,10 @@ void WTrackMenu::slotRemoveFromDisk() { const QList tracksToKeep(trackOperator.getTracksToKeep()); if (tracksToKeep.isEmpty()) { - // All selected tracks could be processed. Finish! - emit restoreCurrentIndex(); + if (m_pTrackModel || restoreViewState) { + // All selected tracks could be processed. Finish! + emit restoreCurrentViewStateOrIndex(); + } return; } // Else show a message with a list of tracks that could not be deleted. @@ -2394,7 +2410,7 @@ void WTrackMenu::slotRemoveFromDisk() { // Required for being able to close the dialog connect(closeBtn, &QPushButton::clicked, &dlgNotDeleted, &QDialog::close); dlgNotDeleted.exec(); - emit restoreCurrentIndex(); + emit restoreCurrentViewStateOrIndex(); } void WTrackMenu::slotShowDlgTrackInfo() { @@ -2543,7 +2559,7 @@ void WTrackMenu::slotRemove() { return; } m_pTrackModel->removeTracks(getTrackIndices()); - emit restoreCurrentIndex(); + emit restoreCurrentViewStateOrIndex(); } void WTrackMenu::slotHide() { @@ -2551,7 +2567,7 @@ void WTrackMenu::slotHide() { return; } m_pTrackModel->hideTracks(getTrackIndices()); - emit restoreCurrentIndex(); + emit restoreCurrentViewStateOrIndex(); } void WTrackMenu::slotUnhide() { @@ -2559,7 +2575,7 @@ void WTrackMenu::slotUnhide() { return; } m_pTrackModel->unhideTracks(getTrackIndices()); - emit restoreCurrentIndex(); + emit restoreCurrentViewStateOrIndex(); } void WTrackMenu::slotPurge() { @@ -2567,7 +2583,7 @@ void WTrackMenu::slotPurge() { return; } m_pTrackModel->purgeTracks(getTrackIndices()); - emit restoreCurrentIndex(); + emit restoreCurrentViewStateOrIndex(); } void WTrackMenu::clearTrackSelection() { diff --git a/src/widget/wtrackmenu.h b/src/widget/wtrackmenu.h index a8c60492856..8a02be5a1f2 100644 --- a/src/widget/wtrackmenu.h +++ b/src/widget/wtrackmenu.h @@ -111,7 +111,8 @@ class WTrackMenu : public QMenu { signals: void loadTrackToPlayer(TrackPointer pTrack, const QString& group, bool play = false); void trackMenuVisible(bool visible); - void restoreCurrentIndex(); + void saveCurrentViewState(); + void restoreCurrentViewStateOrIndex(); private slots: // File diff --git a/src/widget/wtrackproperty.cpp b/src/widget/wtrackproperty.cpp index 36f03ce2a11..f8b6a60b1b9 100644 --- a/src/widget/wtrackproperty.cpp +++ b/src/widget/wtrackproperty.cpp @@ -226,20 +226,30 @@ void WTrackProperty::ensureTrackMenuIsCreated() { this, m_pConfig, m_pLibrary, WTrackMenu::kDeckTrackMenuFeatures); // The show control exists only for main decks. - if (!m_isMainDeck) { - return; + if (m_isMainDeck) { + // When a track menu for this deck is shown/hidden via contextMenuEvent + // or pushbutton, it emits trackMenuVisible(bool). + // The pushbutton is created in BaseTrackPlayer which, on value change requests, + // also emits a signal which is connected to our slotShowTrackMenuChangeRequest(). + connect(m_pTrackMenu, + &WTrackMenu::trackMenuVisible, + this, + [this](bool visible) { + ControlObject::set(ConfigKey(m_group, kShowTrackMenuKey), + visible ? 1.0 : 0.0); + }); } - // When a track menu for this deck is shown/hidden via contextMenuEvent - // or pushbutton, it emits trackMenuVisible(bool). - // The pushbutton is created in BaseTrackPlayer which, on value change requests, - // also emits a signal which is connected to our slotShowTrackMenuChangeRequest(). + // Before and after the loaded tracks file has been removed from disk, + // instruct the library to save and restore the current index for + // keyboard/controller navigation. + connect(m_pTrackMenu, + &WTrackMenu::saveCurrentViewState, + this, + &WTrackProperty::saveCurrentViewState); connect(m_pTrackMenu, - &WTrackMenu::trackMenuVisible, + &WTrackMenu::restoreCurrentViewStateOrIndex, this, - [this](bool visible) { - ControlObject::set(ConfigKey(m_group, kShowTrackMenuKey), - visible ? 1.0 : 0.0); - }); + &WTrackProperty::restoreCurrentViewState); } /// This slot handles show/hide requests originating from both pushbutton changes diff --git a/src/widget/wtrackproperty.h b/src/widget/wtrackproperty.h index 4bc898e6e99..0e97f5efe98 100644 --- a/src/widget/wtrackproperty.h +++ b/src/widget/wtrackproperty.h @@ -61,6 +61,8 @@ class WTrackProperty : public WLabel, public TrackDropTarget { void cloneDeck(const QString& sourceGroup, const QString& targetGroup) override; void setAndConfirmTrackMenuControl(bool visible); void selectedStateChanged(bool state); + void saveCurrentViewState(); + void restoreCurrentViewState(); public slots: void slotTrackLoaded(TrackPointer pTrack); diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index 80352b854c0..5df857fda00 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -353,7 +353,7 @@ void WTrackTableView::initTrackMenu() { // after removing tracks from the view via track menu, restore a usable // selection/currentIndex for navigation via keyboard & controller connect(m_pTrackMenu, - &WTrackMenu::restoreCurrentIndex, + &WTrackMenu::restoreCurrentViewStateOrIndex, this, &WTrackTableView::slotrestoreCurrentIndex); } @@ -468,7 +468,7 @@ void WTrackTableView::slotDeleteTracksFromDisk() { saveCurrentIndex(); m_pTrackMenu->loadTrackModelIndices(indices); m_pTrackMenu->slotRemoveFromDisk(); - // WTrackmenu emits restoreCurrentIndex() + // WTrackmenu emits restoreCurrentViewStateOrIndex() } void WTrackTableView::slotUnhide() { @@ -516,7 +516,7 @@ void WTrackTableView::contextMenuEvent(QContextMenuEvent* event) { // Create the right-click menu m_pTrackMenu->popup(event->globalPos()); - // WTrackmenu emits restoreCurrentIndex() if required + // WTrackmenu emits restoreCurrentViewStateOrIndex() if required } void WTrackTableView::onSearch(const QString& text) { @@ -1140,22 +1140,35 @@ void WTrackTableView::hideOrRemoveSelectedTracks() { } TrackModel::Capability cap; - // Hide is the primary action if allowed. Else we test for remove capability - if (pTrackModel->hasCapabilities(TrackModel::Capability::Hide)) { - cap = TrackModel::Capability::Hide; - } else if (pTrackModel->isLocked()) { // Locked playlists and crates - return; - } + // Remove is the primary action if allowed (playlists and crates). + // Else we test for remove capability. + // In the track menu the hotkey is shown for 'Remove ..' actions, or if there + // is no remove action, for 'Hide ..'. Hence, to match the hotkey, do Hide + // only if the track model doesn't support any Remove actions. if (pTrackModel->hasCapabilities(TrackModel::Capability::Remove)) { cap = TrackModel::Capability::Remove; } else if (pTrackModel->hasCapabilities(TrackModel::Capability::RemoveCrate)) { cap = TrackModel::Capability::RemoveCrate; } else if (pTrackModel->hasCapabilities(TrackModel::Capability::RemovePlaylist)) { cap = TrackModel::Capability::RemovePlaylist; - } else { + } else if (pTrackModel->hasCapabilities(TrackModel::Capability::Hide)) { + cap = TrackModel::Capability::Hide; + } else { // Locked playlists and crates return; } + switch (cap) { + case TrackModel::Capability::Remove: + case TrackModel::Capability::RemoveCrate: + case TrackModel::Capability::RemovePlaylist: { + if (pTrackModel->isLocked()) { + return; + } + default: + break; + } + } + if (pTrackModel->getRequireConfirmationToHideRemoveTracks()) { QString title; QString message; diff --git a/tools/deploy.py b/tools/deploy.py index ac00a2dbaf9..5672333fc9a 100644 --- a/tools/deploy.py +++ b/tools/deploy.py @@ -196,13 +196,14 @@ def prepare_deployment(args): if os.getenv("CI") == "true": # Set GitHub Actions job output print( - "::set-output name=artifact-{}-{}::{}".format( - download_slug, - package_slug, - json.dumps(metadata), - ) + 'echo "{artifact-' + + download_slug + + "-" + + package_slug + + "}={" + + json.dumps(metadata) + + '}" >> $GITHUB_OUTPUT' ) - return 0