diff --git a/CMakeLists.txt b/CMakeLists.txt index 46547ce6106f..ab26d0c99bdd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1455,6 +1455,7 @@ add_library( src/library/searchqueryparser.cpp src/library/serato/seratofeature.cpp src/library/serato/seratoplaylistmodel.cpp + src/library/sidebaritemdelegate.cpp src/library/sidebarmodel.cpp src/library/starrating.cpp src/library/tabledelegates/bpmdelegate.cpp diff --git a/res/images/library/ic_library_refresh.svg b/res/images/library/ic_library_refresh.svg new file mode 100644 index 000000000000..0c4bd2ef3f61 --- /dev/null +++ b/res/images/library/ic_library_refresh.svg @@ -0,0 +1,29 @@ + + Mixxx 1.12+ iconset + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/mixxx.qrc b/res/mixxx.qrc index 37479f4e3bbd..4bdf2bfd48d2 100644 --- a/res/mixxx.qrc +++ b/res/mixxx.qrc @@ -25,6 +25,7 @@ images/library/ic_library_preview_pause.svg images/library/ic_library_preview_play.svg images/library/ic_library_recordings.svg + images/library/ic_library_refresh.svg images/library/ic_library_rhythmbox.svg images/library/ic_library_traktor.svg images/library/ic_library_rekordbox.svg diff --git a/res/skins/LateNight/style_palemoon.qss b/res/skins/LateNight/style_palemoon.qss index 36e463d44243..37ed762dc274 100644 --- a/res/skins/LateNight/style_palemoon.qss +++ b/res/skins/LateNight/style_palemoon.qss @@ -2996,6 +2996,8 @@ WLibrarySidebar { } WLibrarySidebar { outline: none; + show-decoration-selected: 0; + qproperty-bookmarkColor: #ff0000; } /* Selected rows in Tree and Tracks table */ WLibrarySidebar::item:selected, @@ -3019,15 +3021,26 @@ WTrackTableView::item:selected, color: #fff; } -WLibrarySidebar::item:selected:focus { - outline: none; -} -WLibrarySidebar::item:!selected:focus, -/* This, in conjunction with the c++ counterpart, styles only items hovered while dragging */ -WLibrarySidebar[dragHover="true"]::item:hover { - outline: none; - border: 1px solid white; -} +WLibrarySidebar::item { + /* add a dummy border so item text is not shifted when item is focused */ + border-left: 1px solid transparent; + /* add right border so text is not elided when focus border is added */ + border-right: 1px solid transparent; + } + /* subtle highlight if item is selected */ + WLibrarySidebar::item:focus { + outline: none; + border: 1px solid #999; + } + WLibrarySidebar::item:selected:focus { + outline: none; + } + WLibrarySidebar::item:!selected:focus, + /* This, in conjunction with the c++ counterpart, styles only items hovered while dragging */ + WLibrarySidebar[dragHover="true"]::item:hover { + outline: none; + border: 1px solid white; + } WTrackTableView:focus, WLibrarySidebar:focus, @@ -3354,9 +3367,6 @@ QLabel#labelSelectionInfo /* AutoDJ track selection info */ { margin: 4px 5px 5px 1px; } -WLibrarySidebar { - show-decoration-selected: 0; -} /* triangle for closed/opened branches in treeview */ /* closed */ WLibrarySidebar::branch:closed:has-children:!has-siblings:!selected, diff --git a/src/library/browse/browsefeature.cpp b/src/library/browse/browsefeature.cpp index c2d981099ef3..a791167564ea 100644 --- a/src/library/browse/browsefeature.cpp +++ b/src/library/browse/browsefeature.cpp @@ -339,7 +339,7 @@ void BrowseFeature::onRightClickChild(const QPoint& globalPos, const QModelIndex QString path = pItem->getData().toString(); - if (path == QUICK_LINK_NODE || path == DEVICE_NODE) { + if (path == QUICK_LINK_NODE) { return; } @@ -349,9 +349,8 @@ void BrowseFeature::onRightClickChild(const QPoint& globalPos, const QModelIndex QMenu menu(m_pSidebarWidget); - if (pItem->parent()->getData().toString() == QUICK_LINK_NODE || - m_quickLinkList.contains(path)) { - // This is a QuickLink or path is in the Quick Link list + if (pItem->parent()->getData().toString() == QUICK_LINK_NODE) { + // This is a QuickLink menu.addAction(m_pRemoveQuickLinkAction); } else { menu.addAction(m_pAddQuickLinkAction); @@ -414,7 +413,7 @@ std::vector> createRemovableDevices() { // This is called whenever you double click or use the triangle symbol to expand // the subtree. The method will read the subfolders. -void BrowseFeature::onLazyChildExpandation(const QModelIndex& index) { +void BrowseFeature::onLazyChildExpandation(const QModelIndex& index, bool enforceRebuild) { // Caution: Make sure the passed index still exists in the model. // In case it has been removed or replaced, it is still "valid", but // holds dangling internalPointer() that causes a crash. @@ -433,45 +432,103 @@ void BrowseFeature::onLazyChildExpandation(const QModelIndex& index) { // will continue to be valid as long as they can be accessed by the model" QPersistentModelIndex idx(index); if (!idx.isValid()) { - qWarning() << "BrowseFeature::onLazyChildExpandation -> index invalid, abort."; + qWarning() << "BrowseFeature::onLazyChildExpandation -> index invalid, abort" << index; return; } TreeItem* pItem = static_cast(idx.internalPointer()); if (!(pItem && pItem->getData().isValid())) { - qWarning() << "BrowseFeature::onLazyChildExpandation -> no item or no item data, abort."; + qWarning() << "BrowseFeature::onLazyChildExpandation: no item or item data invalid"; return; } - qDebug() << "BrowseFeature::onLazyChildExpandation " << pItem->getLabel() - << " " << pItem->getData(); + qWarning() << "BrowseFeature::onLazyChildExpandation " + << pItem->getLabel() << pItem->getData().toString(); - QString path = pItem->getData().toString(); + const QString path = pItem->getData().toString(); // If the item is a built-in node, e.g., 'QuickLink' return if (path.isEmpty() || path == QUICK_LINK_NODE) { return; } - // Before we populate the subtree, we need to delete old subtrees - m_pSidebarModel->removeRows(0, pItem->childRows(), idx); - // List of subfolders or drive letters std::vector> folders; - // If we are on the special device node if (path == DEVICE_NODE) { + folders = createRemovableDevices(); + } else { + folders = getChildDirectoryItems(path); + } + + // Always remove the childrens' `hasChildren` flag.in order to + // always show the real state for child items that (still) exist. + m_pSidebarModel->removeChildDirsFromCache(QStringList{path}); + + // Build the child tree only if there are currently no folders displayed, + // eg. on initial expand, if user clicked the "Refresh directory tree" action + // or left-clicked the Refresh icon. + // If we detect changed child directories, eg. when users un/mounted devices + // or renamed, added or removed directories outside Mixxx, we show an icon + // on the changed parent item and users can refresh with the menu action. + // TODO Check if performance is an issue, and if yes, if it'd be better if + // we can get a path QStringList instead of constructing all TreeItems. + // + // Note(ronso0) The reason to avoid needless rebuild of the tree is that, + // with SidebarBookmarks, jumping to a bookmark in a collapsed tree would + // cause a crash when the tree rebuild triggered by expandation invalidates + // the index we are currently using in WLibrarySidebar. + // Note: ideally we'd reimplement QTreeView's expand() function and maybe + // others, too, in order to allow us to emit different signals for manual + // and programmatic expandation. Though, this is not an option because all + // this is done in QTreeView's private base class and therefore a no-go. + int childRows = pItem->childRows(); + if (!enforceRebuild && childRows > 0) { + bool needsUpdate = false; + if (childRows == static_cast(folders.size())) { + const auto currChildren = pItem->children(); + for (int i = 0; i < childRows; i++) { + if (currChildren[i]->getData() != folders[i].get()->getData()) { + needsUpdate = true; + break; + } + } + if (!needsUpdate) { + // Nothing to do. + // Also update children's `hasChildren` flag? + return; + } + // Else we found a count or path mismatch and rebuild the tree. + } else { + needsUpdate = true; + } + + if (needsUpdate) { + // Show a warning icon on the parent item. Users may then use the context + // menu -> "Refresh directory tree". + // Or click the Refresh icon. This is handled by SidebarItemDelegate + pItem->setIcon(QIcon(QStringLiteral(":/images/library/ic_library_refresh.svg"))); + pItem->setNeedsUpdate(true); + return; + } + } + + // Reset `needsUpdate` state + pItem->setIcon(QIcon()); + pItem->setNeedsUpdate(false); + + // Before we populate the subtree, we need to delete old subtrees + if (childRows > 0) { + m_pSidebarModel->removeRows(0, childRows, idx); + } #if defined(__LINUX__) + if (path == DEVICE_NODE) { // Tell the model to remove the cached 'hasChildren' states of all sub- // directories when we expand the Device node. // This ensures we show the real dir tree. This is relevant when devices // were unmounted, changed and mounted again. m_pSidebarModel->removeChildDirsFromCache(removableDriveRootPaths()); -#endif - folders = createRemovableDevices(); - } else { - folders = getChildDirectoryItems(path); } - +#endif if (!folders.empty()) { m_pSidebarModel->insertTreeItemRows(std::move(folders), 0, idx); } diff --git a/src/library/browse/browsefeature.h b/src/library/browse/browsefeature.h index d809406a1c0d..927b6a6ef7f1 100644 --- a/src/library/browse/browsefeature.h +++ b/src/library/browse/browsefeature.h @@ -47,7 +47,7 @@ class BrowseFeature : public LibraryFeature { void activate() override; void activateChild(const QModelIndex& index) override; void onRightClickChild(const QPoint& globalPos, const QModelIndex& index) override; - void onLazyChildExpandation(const QModelIndex& index) override; + void onLazyChildExpandation(const QModelIndex& index, bool enforceRebuild = false) override; void slotLibraryScanStarted(); void slotLibraryScanFinished(); void invalidateRightClickIndex(); @@ -82,4 +82,5 @@ class BrowseFeature : public LibraryFeature { TreeItem* m_pQuickLinkItem; QStringList m_quickLinkList; QPointer m_pSidebarWidget; + bool m_forceUpdate; }; diff --git a/src/library/dao/playlistdao.cpp b/src/library/dao/playlistdao.cpp index 477c1047ee76..df1a0944be5d 100644 --- a/src/library/dao/playlistdao.cpp +++ b/src/library/dao/playlistdao.cpp @@ -89,7 +89,7 @@ int PlaylistDAO::createPlaylist(const QString& name, const HiddenType hidden) { int playlistId = query.lastInsertId().toInt(); // Commit the transaction transaction.commit(); - emit added(playlistId); + emit added(playlistId, hidden); return playlistId; } @@ -198,7 +198,8 @@ void PlaylistDAO::deletePlaylist(const int playlistId) { ScopedTransaction transaction(m_database); QSet playedTrackIds; - if (getHiddenType(playlistId) == PLHT_SET_LOG) { + PlaylistDAO::HiddenType type = getHiddenType(playlistId); + if (type == PLHT_SET_LOG) { const QList trackIds = getTrackIds(playlistId); // TODO: QSet::fromList(const QList&) is deprecated and should be @@ -246,7 +247,7 @@ void PlaylistDAO::deletePlaylist(const int playlistId) { } } - emit deleted(playlistId); + emit deleted(playlistId, type); if (!playedTrackIds.isEmpty()) { emit tracksRemovedFromPlayedHistory(playedTrackIds); } @@ -260,6 +261,14 @@ bool PlaylistDAO::deletePlaylists(const QStringList& idStringList) { qInfo() << "Deleting" << idStringList.size() << "playlists"; + PlaylistDAO::HiddenType type = PlaylistDAO::HiddenType::PLHT_UNKNOWN; + // Get playlist type. Assumes playlist batch deletion was invoked + // by the same library feature, ie. all are of same type. + bool ok = false; + int firstId = idStringList.first().toInt(&ok); + if (ok) { + type = getHiddenType(firstId); + } // delete tracks assigned to these playlists auto deleteTracks = FwdSqlQuery(m_database, QString("DELETE FROM PlaylistTracks WHERE playlist_id IN (%1)") @@ -275,7 +284,7 @@ bool PlaylistDAO::deletePlaylists(const QStringList& idStringList) { return false; } - emit deleted(kInvalidPlaylistId); + emit deleted(kInvalidPlaylistId, type); return true; } diff --git a/src/library/dao/playlistdao.h b/src/library/dao/playlistdao.h index f30f89923363..19f36ed52904 100644 --- a/src/library/dao/playlistdao.h +++ b/src/library/dao/playlistdao.h @@ -134,13 +134,17 @@ class PlaylistDAO : public QObject, public virtual DAO { void setAutoDJProcessor(AutoDJProcessor* pAutoDJProcessor); signals: - void added(int playlistId); - void deleted(int playlistId); + // Added/deleted triggers rebuild of the feature's sidebar model. + // Pass the type so receivers (library features) can easily decide + // whether to act or not. + void added(int playlistId, HiddenType type); + void deleted(int playlistId, HiddenType type); void renamed(int playlistId, const QString& newName); void lockChanged(const QSet& playlistIds); void trackAdded(int playlistId, TrackId trackId, int position); void trackRemoved(int playlistId, TrackId trackId, int position); - // added / removed / un/locked. Triggers playlist features to update the sidebar + // Track(s) added/removed or un/locked. Triggers playlist features + // to update the sidebar labels or icons. void playlistContentChanged(const QSet& playlistIds); // Separate signals for PlaylistTableModel void tracksAdded(const QSet& playlistIds); diff --git a/src/library/library.cpp b/src/library/library.cpp index 36710fc999cc..79366845cbb1 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -267,9 +267,17 @@ Library::Library( m_editMetadataSelectedClick = m_pConfig->getValue( kEditMetadataSelectedClickConfigKey, kEditMetadataSelectedClickDefault); + + if (m_pSidebarModel && m_pConfig) { + m_pSidebarModel->loadBookmarksFromConfig(m_pConfig); + } } -Library::~Library() = default; +Library::~Library() { + if (m_pSidebarModel && m_pConfig) { + m_pSidebarModel->saveBookmarksToConfig(m_pConfig); + } +} TrackCollectionManager* Library::trackCollectionManager() const { // Cannot be implemented inline due to forward declarations diff --git a/src/library/librarycontrol.cpp b/src/library/librarycontrol.cpp index efac7b356b0b..8f8244746c7e 100644 --- a/src/library/librarycontrol.cpp +++ b/src/library/librarycontrol.cpp @@ -524,6 +524,47 @@ LibraryControl::LibraryControl(Library* pLibrary) this, &LibraryControl::slotLoadSelectedIntoFirstStopped); + m_pBookmarkNext = std::make_unique( + ConfigKey("[Library]", "bookmark_next")); + connect(m_pBookmarkNext.get(), + &ControlPushButton::valueChanged, + this, + [this](double value) { + VERIFY_OR_DEBUG_ASSERT(m_pSidebarWidget) { + return; + } + if (value > 0) { + m_pSidebarWidget->slotGoToNextPrevBookmark(1); + } + }); + m_pBookmarkPrev = std::make_unique( + ConfigKey("[Library]", "bookmark_prev")); + connect(m_pBookmarkPrev.get(), + &ControlPushButton::valueChanged, + this, + [this](double value) { + VERIFY_OR_DEBUG_ASSERT(m_pSidebarWidget) { + return; + } + if (value > 0) { + m_pSidebarWidget->slotGoToNextPrevBookmark(-1); + } + }); + m_pBookmarkSelect = std::make_unique( + ConfigKey("[Library]", "bookmark_selector"), false); + connect(m_pBookmarkSelect.get(), + &ControlEncoder::valueChanged, + this, + [this](double steps) { + VERIFY_OR_DEBUG_ASSERT(m_pSidebarWidget) { + return; + } + int iSteps = static_cast(steps); + if (iSteps) { + m_pSidebarWidget->slotGoToNextPrevBookmark(static_cast(steps)); + } + }); + #ifdef MIXXX_USE_QML if (!CmdlineArgs::Instance().isQml()) #endif diff --git a/src/library/librarycontrol.h b/src/library/librarycontrol.h index ecf6e1370bc9..ac488cc5fa57 100644 --- a/src/library/librarycontrol.h +++ b/src/library/librarycontrol.h @@ -216,6 +216,10 @@ class LibraryControl : public QObject { std::unique_ptr m_pToggleSidebarItem; std::unique_ptr m_pLoadSelectedIntoFirstStopped; + std::unique_ptr m_pBookmarkNext; + std::unique_ptr m_pBookmarkPrev; + std::unique_ptr m_pBookmarkSelect; + // Library widgets WLibrary* m_pLibraryWidget; WLibrarySidebar* m_pSidebarWidget; diff --git a/src/library/libraryfeature.cpp b/src/library/libraryfeature.cpp index 7c88fbf93bdc..06638e3dac90 100644 --- a/src/library/libraryfeature.cpp +++ b/src/library/libraryfeature.cpp @@ -27,9 +27,10 @@ LibraryFeature::LibraryFeature( m_pLibrary(pLibrary), m_pConfig(pConfig), m_iconName(iconName) { - if (!m_iconName.isEmpty()) { - m_icon = QIcon(kIconPath.arg(m_iconName)); + VERIFY_OR_DEBUG_ASSERT(!m_iconName.isEmpty()) { + return; } + m_icon = QIcon(kIconPath.arg(m_iconName)); } void LibraryFeature::selectAndActivate(const QModelIndex& index) { diff --git a/src/library/libraryfeature.h b/src/library/libraryfeature.h index 4e8390a02e2c..0b7c9123d637 100644 --- a/src/library/libraryfeature.h +++ b/src/library/libraryfeature.h @@ -87,6 +87,10 @@ class LibraryFeature : public QObject { virtual bool hasTrackTable() { return false; } + virtual bool isItemDataUnique(const QVariant& data) const { + Q_UNUSED(data); + return true; + } protected: QStringList getPlaylistFiles() const { @@ -134,9 +138,13 @@ class LibraryFeature : public QObject { Q_UNUSED(index); } // Only implement this, if using incremental or lazy childmodels, see BrowseFeature. - // This method is executed whenever you **double** click child items - virtual void onLazyChildExpandation(const QModelIndex& index) { + // This is called whenever you manually expand the subtree (double click on item + // or click on the triangle icon) and after programmatic expandation with QTreeView's + // expand() or scrollTo() (with EnsureVisible hint the latter auto-expands all + // of the index' parents). + virtual void onLazyChildExpandation(const QModelIndex& index, bool enforceRebuild = false) { Q_UNUSED(index); + Q_UNUSED(enforceRebuild); } signals: void showTrackModel(QAbstractItemModel* model, bool restoreState = true); diff --git a/src/library/sidebaritemdelegate.cpp b/src/library/sidebaritemdelegate.cpp new file mode 100644 index 000000000000..7acb42075f13 --- /dev/null +++ b/src/library/sidebaritemdelegate.cpp @@ -0,0 +1,70 @@ +#include "library/sidebaritemdelegate.h" + +#include +#include +#include + +#include "library/sidebarmodel.h" +#include "moc_sidebaritemdelegate.cpp" +#include "util/assert.h" +#include "widget/wlibrarysidebar.h" + +SidebarItemDelegate::SidebarItemDelegate( + WLibrarySidebar* pSidebarWidget, + SidebarModel* pSidebarModel) + : QStyledItemDelegate(pSidebarWidget), + m_pSidebarModel(pSidebarModel) { + DEBUG_ASSERT(m_pSidebarModel); +} + +// Used to paint SidebarBookmarks +void SidebarItemDelegate::paint( + QPainter* pPainter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const { + QStyledItemDelegate::paint(pPainter, option, index); + + // If the item is a bookmark, draw the indicator on top of the qss style. + // We draw a rectangle a the left, narrow enough to not cover the label and + // inset by 1px to not cover the focus border. + if (m_bookmarkColor.isValid() && m_pSidebarModel->indexIsBookmark(index)) { + pPainter->fillRect( + option.rect.x(), + option.rect.y() + 1, + 3, // width + option.rect.height() - 2, + m_bookmarkColor); + } +} + +// Used to catch clicks on TreeItem icons. Implemented only for BrowseFeature +// folder items that need an update. Click does force-rebuild the child tree. +bool SidebarItemDelegate::editorEvent(QEvent* pEvent, + QAbstractItemModel* pModel, + const QStyleOptionViewItem& option, + const QModelIndex& index) { + // Only act on left click + // Note: act on release in case we implement drag'n'drop for items. + if (pEvent->type() == QEvent::MouseButtonPress) { + if (m_pSidebarModel->data(index, SidebarModel::NeedsUpdateRole).toBool()) { + QMouseEvent* pME = static_cast(pEvent); + // Right click should be be handled by WLibrarySidebar and sidebar models! + VERIFY_OR_DEBUG_ASSERT(pME->button() == Qt::LeftButton) { + return false; + } + // Check if it's a click on the icon + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + const QWidget* pWidget = opt.widget; + const QRect iconRect = pWidget->style()->subElementRect( + QStyle::SE_ItemViewItemDecoration, + &opt, + pWidget); + if (iconRect.contains(pME->pos())) { + // Enforce update (tree rebuild) + m_pSidebarModel->updateItem(index); + } + } + } + return QStyledItemDelegate::editorEvent(pEvent, pModel, option, index); +} diff --git a/src/library/sidebaritemdelegate.h b/src/library/sidebaritemdelegate.h new file mode 100644 index 000000000000..a58387087d40 --- /dev/null +++ b/src/library/sidebaritemdelegate.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +class SidebarModel; +class WLibrarySidebar; + +class SidebarItemDelegate : public QStyledItemDelegate { + Q_OBJECT + public: + explicit SidebarItemDelegate( + WLibrarySidebar* pSidebarwidget, + SidebarModel* pSidebarModel); + ~SidebarItemDelegate() override = default; + + void paint( + QPainter* pPainter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const override; + + void setBookmarkColor(const QColor& color) { + if (color.isValid()) { + m_bookmarkColor = color; + } + } + + bool editorEvent(QEvent* pEvent, + QAbstractItemModel* pModel, + const QStyleOptionViewItem& option, + const QModelIndex& index) override; + + private: + SidebarModel* m_pSidebarModel; // shared_ptr? + QColor m_bookmarkColor; +}; diff --git a/src/library/sidebarmodel.cpp b/src/library/sidebarmodel.cpp index 189e4270029b..219e3c0d7a58 100644 --- a/src/library/sidebarmodel.cpp +++ b/src/library/sidebarmodel.cpp @@ -25,7 +25,7 @@ constexpr bool kDebug = false; SidebarModel::SidebarModel( QObject* parent) : QAbstractItemModel(parent), - m_iDefaultSelectedIndex(0), + m_iDefaultSelectedIndex(0), // Tracks / MixxxLibraryFeature m_pressedUntilClickedTimer(new QTimer(this)) { m_pressedUntilClickedTimer->setSingleShot(true); connect(m_pressedUntilClickedTimer, @@ -110,10 +110,9 @@ QModelIndex SidebarModel::index(int row, int column, } if (parent.isValid()) { - /* If we have selected the root of a library feature at position 'row' - * its internal pointer is the current sidebar object model - * we return its associated childmodel - */ + // If we have selected the root of a library feature at position 'row', + // its internal pointer is a pointer to the current sidebarmodel object. + // We return its associated childmodel index. if (parent.internalPointer() == this) { const QAbstractItemModel* childModel = m_sFeatures[parent.row()]->sidebarModel(); QModelIndex childIndex = childModel->index(row, column); @@ -124,7 +123,7 @@ QModelIndex SidebarModel::index(int row, int column, return QModelIndex(); } } else { - // We have selected an item within the childmodel + // We have selected an item within the childmodel. // This item has always an internal pointer of (sub)type TreeItem TreeItem* pTreeItem = static_cast(parent.internalPointer()); if (row < pTreeItem->childRows()) { @@ -146,14 +145,11 @@ QModelIndex SidebarModel::getFeatureRootIndex(LibraryFeature* pFeature) { if constexpr (kDebug) { qDebug() << "SidebarModel::getFeatureRootIndex for" << pFeature->title().toString(); } - QModelIndex ind; - for (int i = 0; i < m_sFeatures.size(); ++i) { - if (m_sFeatures[i] == pFeature) { - ind = index(i, 0); - break; - } + int featureRow = m_sFeatures.indexOf(pFeature); + VERIFY_OR_DEBUG_ASSERT(featureRow != -1) { + return {}; } - return ind; + return index(featureRow, 0); } void SidebarModel::clear(const QModelIndex& index) { @@ -178,42 +174,43 @@ QModelIndex SidebarModel::parent(const QModelIndex& index) const { if constexpr (kDebug) { qDebug() << "SidebarModel::parent index=" << index; } - if (index.isValid()) { - // If we have selected the root of a library feature - // its internal pointer is the current sidebar object model - // A root library feature has no parent and thus we return - // an invalid QModelIndex - if (index.internalPointer() == this) { - return QModelIndex(); - } else { - TreeItem* pTreeItem = static_cast(index.internalPointer()); - if (pTreeItem == nullptr) { - return QModelIndex(); - } - TreeItem* pTreeItemParent = pTreeItem->parent(); - // if we have selected an item at the first level of a childnode - - if (pTreeItemParent) { - if (pTreeItemParent->isRoot()) { - LibraryFeature* pFeature = pTreeItem->feature(); - for (int i = 0; i < m_sFeatures.size(); ++i) { - if (pFeature == m_sFeatures[i]) { - // create a ModelIndex for parent 'this' having a - // library feature at position 'i' - // `this` is const, but the function expects a - // non-const pointer. - // TODO: Check if we can get rid of this const cast - // somehow. - return createIndex(i, 0, const_cast(this)); - } - } - } - // if we have selected an item at some deeper level of a childnode - return createIndex(pTreeItemParent->parentRow(), 0, pTreeItemParent); - } + if (!index.isValid()) { + return {}; + } + + // If we have selected the root of a library feature, + // its internal pointer is the current sidebar object model. + // A root library feature has no parent and thus we return + // an invalid QModelIndex. + if (index.internalPointer() == this) { + return {}; + } + + TreeItem* pTreeItem = static_cast(index.internalPointer()); + VERIFY_OR_DEBUG_ASSERT(pTreeItem != nullptr) { + return {}; + } + TreeItem* pParentItem = pTreeItem->parent(); + VERIFY_OR_DEBUG_ASSERT(pParentItem != nullptr) { + return {}; + } + if (pParentItem->isRoot()) { + // If we have selected an item at the first level of a childnode, + // Create a ModelIndex for parent 'this' having a + // library feature at position 'i'. + // `this` is const, but the function expects a + // non-const pointer. + // TODO: Check if we can get rid of this const cast + // somehow. + LibraryFeature* pFeature = pTreeItem->feature(); + int featureRow = m_sFeatures.indexOf(pFeature); + VERIFY_OR_DEBUG_ASSERT(featureRow != -1) { + return {}; } + return createIndex(featureRow, 0, const_cast(this)); } - return QModelIndex(); + // If we have selected an item at some deeper level of a childnode + return createIndex(pParentItem->parentRow(), 0, pParentItem); } int SidebarModel::rowCount(const QModelIndex& parent) const { @@ -274,7 +271,7 @@ QVariant SidebarModel::data(const QModelIndex& index, int role) const { } if (index.internalPointer() == this) { - //If it points to SidebarModel + // If it points to SidebarModel this is a root item. switch (role) { case Qt::DisplayRole: return m_sFeatures[index.row()]->title(); @@ -312,6 +309,9 @@ QVariant SidebarModel::data(const QModelIndex& index, int role) const { return pTreeItem->getIcon(); case SidebarModel::DataRole: return pTreeItem->getData(); + case SidebarModel::NeedsUpdateRole: + // True only for BrowseFeature items that need an update + return pTreeItem->needsUpdate(); case SidebarModel::IconNameRole: // TODO: Add support for icon names in tree items default: @@ -367,33 +367,61 @@ void SidebarModel::clicked(const QModelIndex& index) { } } -/// Invoked by double click and click on tree node expand icons +/// Invoked by double click on child items, click on tree node expand icons and +/// when jumping to a child item in a collapsed branch. void SidebarModel::doubleClicked(const QModelIndex& index) { stopPressedUntilClickedTimer(); - if (index.isValid()) { - if (index.internalPointer() == this) { - return; - } else { - TreeItem* pTreeItem = static_cast(index.internalPointer()); - if (pTreeItem) { - LibraryFeature* pFeature = pTreeItem->feature(); - pFeature->onLazyChildExpandation(index); - } + if (!index.isValid()) { + return; + } + if (index.internalPointer() == this) { + // Index is a root index and QTreeView already did expand it, + // so there's nothing to do for us. + return; + } else { + TreeItem* pTreeItem = static_cast(index.internalPointer()); + VERIFY_OR_DEBUG_ASSERT(pTreeItem) { + return; } + LibraryFeature* pFeature = pTreeItem->feature(); + pFeature->onLazyChildExpandation(index); + } +} + +bool SidebarModel::indexNeedsUpdate(const QModelIndex& index) const { + const QVariant updateData = data(index, NeedsUpdateRole); + if (updateData.isValid() && updateData.canConvert()) { + return updateData.toBool(); + } + return false; +} + +/// Invoked by click on Refresh icon of BrowseFeature items whose child tree +/// is outdated. Triggers force-rebuild of the chidl tree. +void SidebarModel::updateItem(const QModelIndex& index) { + stopPressedUntilClickedTimer(); + if (!index.isValid()) { + return; + } + TreeItem* pTreeItem = static_cast(index.internalPointer()); + if (pTreeItem) { + LibraryFeature* pFeature = pTreeItem->feature(); + pFeature->onLazyChildExpandation(index, true /* enforce rebuild */); } } void SidebarModel::rightClicked(const QPoint& globalPos, const QModelIndex& index) { stopPressedUntilClickedTimer(); - if (index.isValid()) { - if (index.internalPointer() == this) { - m_sFeatures[index.row()]->onRightClick(globalPos); - } else { - TreeItem* pTreeItem = static_cast(index.internalPointer()); - if (pTreeItem) { - LibraryFeature* pFeature = pTreeItem->feature(); - pFeature->onRightClickChild(globalPos, index); - } + if (!index.isValid()) { + return; + } + if (index.internalPointer() == this) { + m_sFeatures[index.row()]->onRightClick(globalPos); + } else { + TreeItem* pTreeItem = static_cast(index.internalPointer()); + if (pTreeItem) { + LibraryFeature* pFeature = pTreeItem->feature(); + pFeature->onRightClickChild(globalPos, index); } } } @@ -489,19 +517,16 @@ bool SidebarModel::dragMoveAccept(const QModelIndex& index, const QList& u /// Translates an index from the child models to an index of the sidebar models QModelIndex SidebarModel::translateSourceIndex(const QModelIndex& index) { - /* These method is called from the slot functions below. - * QObject::sender() return the object which emitted the signal - * handled by the slot functions. - - * For child models, this always the child models itself - */ - - const QAbstractItemModel* model = qobject_cast(sender()); - VERIFY_OR_DEBUG_ASSERT(model != nullptr) { + // These method is called from the slot functions below. + // QObject::sender() return the object which emitted the signal + // handled by the slot functions. + // For child models, this always the child models itself + const QAbstractItemModel* pModel = qobject_cast(sender()); + VERIFY_OR_DEBUG_ASSERT(pModel != nullptr) { return QModelIndex(); } - return translateIndex(index, model); + return translateIndex(index, pModel); } QModelIndex SidebarModel::translateIndex( @@ -522,22 +547,21 @@ QModelIndex SidebarModel::translateIndex( } void SidebarModel::slotDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { - // qDebug() << "slotDataChanged topLeft:" << topLeft << "bottomRight:" << bottomRight; + // qDebug() << "slotDataChanged topLeft:" << topLeft; + // qDebug() << " bottomRight:" << bottomRight; QModelIndex topLeftTranslated = translateSourceIndex(topLeft); QModelIndex bottomRightTranslated = translateSourceIndex(bottomRight); emit dataChanged(topLeftTranslated, bottomRightTranslated); } void SidebarModel::slotRowsAboutToBeInserted(const QModelIndex& parent, int start, int end) { - //qDebug() << "slotRowsABoutToBeInserted" << parent << start << end; - + // qDebug() << "slotRowsABoutToBeInserted" << parent << start << end; QModelIndex newParent = translateSourceIndex(parent); beginInsertRows(newParent, start, end); } void SidebarModel::slotRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { - //qDebug() << "slotRowsABoutToBeRemoved" << parent << start << end; - + // qDebug() << "slotRowsABoutToBeRemoved" << parent << start << end; QModelIndex newParent = translateSourceIndex(parent); beginRemoveRows(newParent, start, end); } @@ -547,32 +571,33 @@ void SidebarModel::slotRowsInserted(const QModelIndex& parent, int start, int en Q_UNUSED(start); Q_UNUSED(end); // qDebug() << "slotRowsInserted" << parent << start << end; - // QModelIndex newParent = translateSourceIndex(parent); + QModelIndex newParent = translateSourceIndex(parent); endInsertRows(); + maybeUpdateBookmarkIndices(newParent); } void SidebarModel::slotRowsRemoved(const QModelIndex& parent, int start, int end) { Q_UNUSED(parent); Q_UNUSED(start); Q_UNUSED(end); - //qDebug() << "slotRowsRemoved" << parent << start << end; - //QModelIndex newParent = translateSourceIndex(parent); + // qDebug() << "slotRowsRemoved" << parent << start << end; + // QModelIndex newParent = translateSourceIndex(parent); endRemoveRows(); } void SidebarModel::slotModelAboutToBeReset() { + // qDebug() << "slotModelAboutToBeReset"; beginResetModel(); } void SidebarModel::slotModelReset() { + // qDebug() << "slotModelReset"; endResetModel(); } -/* - * Call this slot whenever the title of the feature has changed. - * See RhythmboxFeature for an example, in which the title becomes '(loading) Rhythmbox' - * If selectFeature is true, the feature is selected when the title change occurs. - */ +// Call this slot whenever the title of the feature has changed. +// See RhythmboxFeature for an example, in which the title becomes '(loading) Rhythmbox' +// If selectFeature is true, the feature is selected when the title change occurs. void SidebarModel::slotFeatureIsLoading(LibraryFeature* pFeature, bool selectFeature) { featureRenamed(pFeature); if (selectFeature) { @@ -580,9 +605,6 @@ void SidebarModel::slotFeatureIsLoading(LibraryFeature* pFeature, bool selectFea } } -/* Tobias: This slot is somewhat redundant but I decided - * to leave it for code readability reasons - */ void SidebarModel::slotFeatureLoadingFinished(LibraryFeature* pFeature) { featureRenamed(pFeature); slotFeatureSelect(pFeature); @@ -614,3 +636,466 @@ void SidebarModel::slotFeatureSelect(LibraryFeature* pFeature, } emit selectIndex(ind, scrollTo); } + +void SidebarModel::toggleBookmarkByIndex(const QModelIndex& index) { + if (!index.isValid()) { + // qWarning() << "toggleBookmarkByIndex ! index invalid" << index; + return; + } + + qWarning() << "toggleBookmarkByIndex" << index; + SidebarBookmark bookmark = createBookmarkFromIndex(index); + if (m_bookmarks.contains(bookmark)) { + // Remove bookmark and index + m_bookmarks.removeOne(bookmark); + qWarning() << "--- removed" << bookmark; + } else { + m_bookmarks.append(bookmark); + qWarning() << "+++ added" << bookmark; + } + m_bookmarkIndices = sortBookmarksUpdateIndices(&m_bookmarks); +} + +QModelIndexList SidebarModel::sortBookmarksUpdateIndices(QList* pBookmarks) { + // Sort by position in the tree so getNextPrevBookmarkIndex() + // switches to bookmark below/above in a predictable manner: + // feature row -> child level -> parent row + std::sort(pBookmarks->begin(), pBookmarks->end()); + // Update indices. Only add valid indices -- invalid means the bookmark + // wasn't found after last child model update. + QModelIndexList newBookmarkIndices; + // qWarning() << "Updating Sidebar bookmark indices"; + for (const auto& bm : std::as_const(*pBookmarks)) { + if (bm.index.isValid()) { + // qWarning() << " valid bm index, added" << bm.index; + newBookmarkIndices.append(bm.index); + } else { + // qWarning() << " ! bm index invalid" << bm.index; + } + } + // qWarning() << "Sidebar bookmark indices updated"; + return newBookmarkIndices; +} + +QModelIndex SidebarModel::getNextPrevBookmarkIndex(const QModelIndex& currIndex, int direction) { + if (!currIndex.isValid() || direction == 0) { + qWarning() << "SidebarModel::getNextPrevBookmarkIndex: invalid index " + "or dir == 0" + << currIndex; + return {}; + } + if (m_bookmarks.isEmpty()) { + qWarning() << "SidebarModel::getNextPrevBookmarkIndex: no bookmarks stored"; + return {}; + } + + if (m_bookmarkIndices.size() == 1 && currIndex == m_bookmarkIndices[0]) { + // We already have the only bookmark selected. + // Return invalid index so the sidebar does not reload the current view. + qWarning() << "SidebarModel::getNextPrevBookmarkIndex: already on only bookmark"; + return {}; + } + + // Do single steps regardless the input. + direction = direction > 0 ? 1 : -1; + qWarning("SidebarModel::getNextPrevBookmarkIndex, dir: %d", direction); + + int currPos = 0; + bool forward = direction > 0; + const int numItems = m_bookmarks.size(); + // Create a bookmark from the current position + const SidebarBookmark tempBM = createBookmarkFromIndex(currIndex); + if (m_bookmarks.contains(tempBM)) { + currPos = m_bookmarks.indexOf(tempBM); + // qWarning(" we're on bookmark, curr pos: %d", currPos); + } else { + // We're not on a bookmark. + // In order to get true prev/next (up/down) behavior from the current + // position, we clone m_bookmarks, add the temporary bookmark, + // sort and get the list index of the current position. + auto tempBookmarks = m_bookmarks; + tempBookmarks.append(tempBM); + sortBookmarksUpdateIndices(&tempBookmarks); + currPos = tempBookmarks.indexOf(tempBM); + // qWarning(" not on a bookmark, creates temp bookmark, curr pos: %d", currPos); + if (forward) { + currPos--; + } + // qWarning(" -> adjusted curr pos to %d", currPos); + } + // Now we have a valid start position in the bookmark list. + // Calculate the target position. + int targetPos = currPos + direction; + // qWarning(" targetPos orig: %d | numItems: %d", targetPos, numItems); + + SidebarBookmark targetBM; + QModelIndex targetIndex; + int attempt = 0; + while (attempt < numItems) { + // qWarning(" find targetBm -- attempt %d | targetPos: %d", attempt, targetPos); + // Check if we need to warp around + if (targetPos < 0 || targetPos >= numItems) { + forward ? targetPos = 0 : targetPos = numItems - 1; + // qWarning(" targetPos < 0 || >= numItems, adjust to %d", targetPos); + } + targetBM = m_bookmarks[targetPos]; + // qWarning() << " targetBm:" << targetBM; + if (targetBM.isValid()) { + // qWarning() << " -> targetBm valid"; + if (targetBM.childLevel == 0) { + // If this is root item we can simply create an index, no need + // for findBookmarkIndex(). + // qWarning(" bookmark is root item %d", targetBM.featureRow); + targetIndex = index(targetBM.featureRow, 0); + } else { + // Else we look up the bookmark inside the child model. + // This may fail, eg. when an item for a bookmark restored from + // config is not present (yet), like Computer items in branches + // that have not been built, yet. + // qWarning(" bookmark is child at level %d", targetBM.childLevel); + const auto bmIndex = findBookmarkIndex(targetBM); + // qWarning() << " bmIndex:" << bmIndex; + targetIndex = translateChildIndex(bmIndex); + // qWarning() << " > translateChIdx:" << targetIndex; + } + } else { + // qWarning() << " -> targetBm NOT valid, continue"; + } + if (targetIndex.isValid()) { + break; + } + forward ? targetPos++ : targetPos--; + attempt++; + } + + if (targetIndex != targetBM.index) { + // Lookup bookmark. + // If found, replace with fresh bookmark, ie. update all properties at once. + // (just in case the index changed and we didn't notice) + // Else just invalidate it's index so we know that we should skip it when + // updating the index list. + if (targetIndex.isValid()) { + targetBM = createBookmarkFromIndex(translateSourceIndex(targetIndex)); + } else { + targetBM.index = QModelIndex(); + } + sortBookmarksUpdateIndices(&m_bookmarks); + } + return targetIndex; +} + +/// Invoked by rowsInserted(). A range of sidebar indices of a childmodel has been +/// rebuilt. Some stored indices may now be invalid so we need to reassociate +/// bookmarks with new indices and update the quick-lookup index list. +/// Happens when playlists or crates are added/removedm, when a History playlist +/// has been moved into a YEAR group or when the BrowseFeature tree is rebuilt. +// +// TODO(ronso0) Implement some lock/wait mechanism to avoid concurrent access to +// m_bookmarks/m_bookmarkIndices. Caller is in same thread so QMutex won't work. +// Reason: previously, deleting a playlist caused both PlaylistFeature and SetlogFeauture +// to rebuilt their child models, even though only one of them can be affected. +// This is now fixed, but I didn't check with other features, hence can't guarantee +// singular/safe access. +void SidebarModel::maybeUpdateBookmarkIndices(const QModelIndex& parentIndex) { + qWarning() << "maybeUpdateBookmarkIndices" << parentIndex; + if (m_bookmarkIndices.isEmpty()) { + qWarning() << " ! bm indices empty"; + return; + } + // Collect the start parameters for findBookmarkIndex() + int featureRow = -1; + if (parentIndex.internalPointer() == this) { + featureRow = parentIndex.row(); + } else { + const auto* pTreeItem = static_cast(parentIndex.internalPointer()); + VERIFY_OR_DEBUG_ASSERT(pTreeItem) { + return; + } + featureRow = m_sFeatures.indexOf(pTreeItem->feature()); + } + + bool needsUpdate = false; + for (auto& bm : m_bookmarks) { + if (bm.featureRow != featureRow) { + continue; + } + // qWarning() << " -> look for" << bm; + needsUpdate = true; + // Lookup bookmark. + // If found, replace with fresh bookmark, ie. update all properties at once. + // Else just invalidate it's index so we know that we should skip it when + // updating the index list. + const auto bmIndex = findBookmarkIndex(bm); + if (bmIndex.isValid()) { + bm = createBookmarkFromIndex(translateSourceIndex(bmIndex)); + // qWarning() << " >> updated" << bm << "in" << pFeature->title().toString(); + } else { + // qWarning() << " >> no match for" << bm << "in" << pFeature->title().toString(); + bm.index = QModelIndex(); + } + } + + if (!needsUpdate) { + // no hit for affected feature, nothing to do + return; + } + + // Note: Don't remove missing bookmarks. + // BrowseFeature bookmarks may be 'missing' after collapsing and + // re-expanding a directory tree one or more levels above a bookmark + // because expanding an item only rebuilds the next sublevel, so + // bookmarked items on lower levels are simply not there, yet. + + m_bookmarkIndices = sortBookmarksUpdateIndices(&m_bookmarks); +} + +/// Try to find the matching TreeItem in a feature's childmodel. +/// Return its index when found, else return invalid QModelIndex(). +/// Scans the entire tree recursively, either by item data or label. +// TODO Do we need to store an index list at all when using match()? +// Compare performance of list vs. match() for each next/prev move. +QModelIndex SidebarModel::findBookmarkIndex(const SidebarBookmark& bookmark) { + qWarning() << " findBookmarkIndex" << bookmark; + VERIFY_OR_DEBUG_ASSERT(bookmark.isValid()) { + return {}; + } + LibraryFeature* pFeature = m_sFeatures[bookmark.featureRow]; + TreeItemModel* pChildModel = pFeature->sidebarModel(); + DEBUG_ASSERT(pChildModel); + const QModelIndex rootIndex = index(bookmark.featureRow, 0); + QModelIndexList results; + if (bookmark.data.isValid() && pFeature->isItemDataUnique(bookmark.data)) { + // qWarning() << " -> child data of" << pFeature->title() << "is unique"; + // Search for matching data + results = pChildModel->match( + rootIndex, + TreeItemModel::kDataRole, + bookmark.data, + 1, + Qt::MatchWrap | Qt::MatchExactly | Qt::MatchRecursive); + } else { + // qWarning() << " -> child data of" << pFeature->title() << "is NOT unique"; + // Search for label match. + // This covers root items, Tracks Missing/Hidden, AutoDJ Crates + // and History's YEAR nodes. + results = pChildModel->match( + rootIndex, + Qt::DisplayRole, + bookmark.label, + 1, + Qt::MatchWrap | Qt::MatchExactly | Qt::MatchRecursive); + } + + if (!results.isEmpty()) { + // qWarning() << " -> result found"; + return results.front(); + } + // qWarning() << " -> no result found, abort"; + return {}; +} + +SidebarBookmark SidebarModel::createBookmarkFromIndex(const QModelIndex& index) { + if (!index.isValid()) { + return {}; + } + + qWarning() << "createBookmarkFromIndex" << index; + SidebarBookmark bookmark; + if (index.internalPointer() == this) { + // qWarning() << "-> int.pointer = this:" << index.internalPointer(); + LibraryFeature* pFeature = m_sFeatures[index.row()]; + bookmark = SidebarBookmark( + index.row(), + 0, + 0, + QVariant(), + // pFeature->title() is not const at runtime, hence not suitable as identifier! + // For example "Tracks (numTracks)" or dynamically loading features + pFeature->iconName(), + index); + } else { + // qWarning() << "-> int.pointer != this:" << index.internalPointer(); + TreeItem* pTreeItem = static_cast(index.internalPointer()); + VERIFY_OR_DEBUG_ASSERT(pTreeItem) { + return {}; + } + bookmark.featureRow = m_sFeatures.indexOf(pTreeItem->feature()); + bookmark.childLevel = pTreeItem->childLevel(); + bookmark.parentRow = pTreeItem->parentRow(); + const auto& data = pTreeItem->getData(); + if (data.isValid() && pTreeItem->isDataUniqueInFeature()) { + // qWarning() << "-> data valid:" << data; + bookmark.data = data; + } else { + bookmark.label = pTreeItem->getLabel(); + // qWarning() << "-> data invalid, use label:" << bookmark.label; + } + bookmark.index = index; + } + // qWarning() << " >> created" << bookmark; + DEBUG_ASSERT(bookmark.isValid()); + return bookmark; +} + +bool SidebarModel::indexIsBookmark(const QModelIndex& index) const { + if (!index.isValid()) { + return false; + } + if (m_bookmarkIndices.contains(index)) { + // qWarning() << "--* indexIsBookmark?" << index << index.internalPointer(); + } + return m_bookmarkIndices.contains(index); +} + +void SidebarModel::saveBookmarksToConfig(UserSettingsPointer pConfig) { + if (pConfig == nullptr || m_bookmarks.isEmpty()) { + return; + } + + // Bookmark identifiers are either + // [item] [value] + // int 0 = feature root item + // └> feature row index + // int-int-int string = child item + // | | | └> data of TreeItem, int-based id, or QString + // | | └> data type: 0 data as QVariant(int) + // | | 1 data a QVariant(QString) + // | └> child level + // └> feature row index + + // qWarning() << "."; + // qWarning() << "save bookmarks to config:"; + const QString group = QStringLiteral("[SidebarBookmarks]"); + for (const auto& bm : std::as_const(m_bookmarks)) { + if (bm.childLevel == 0) { // feature root item + // qWarning() << " root item" << bm.label; + pConfig->setValue(ConfigKey(group, QString::number(bm.featureRow)), QString("---")); + } else { // child item + // qWarning() << " child item" << bm; + // check data type of feature + QVariant idxData = data(bm.index, SidebarModel::DataRole); + if (!idxData.isValid() || idxData.isNull()) { + // each child item must have data + // qWarning() << " ! invalid data"; + continue; + } + int dataType = -1; + QStringList dataStrList; + QString data; + if (idxData.canConvert()) { + bool okay = false; + int dataInt = idxData.toInt(&okay); + if (okay) { + // qWarning() << " -> int data:" << dataInt; + dataType = 0; + data = QString::number(dataInt); + } + } + if (dataType == -1) { + if (idxData.canConvert()) { + dataType = 1; + data = idxData.toString(); + // qWarning() << " -> string data:" << data; + } else { + // qWarning() << " ! unknown data type:" << idxData; + continue; + } + } + dataStrList << QString::number(bm.featureRow); + dataStrList << QString::number(bm.childLevel); + dataStrList << QString::number(dataType); + const QString item = dataStrList.join('-'); + // qWarning() << " save bookmark as" << item << data; + pConfig->setValue(ConfigKey(group, item), data); + } + } +} + +void SidebarModel::loadBookmarksFromConfig(UserSettingsPointer pConfig) { + if (pConfig == nullptr) { + return; + } + + // Bookmark identifiers are either + // [item] [value] + // int 0 = feature root item + // └> feature row index + // int-int-int string = child item + // | | | └> data of TreeItem, int-based id, or QString + // | | └> data type: 0 data as QVariant(int) + // | | 1 data a QVariant(QString) + // | └> child level + // └> feature row index + + // qWarning() << "."; + // qWarning() << "read bookmarks from config:"; + + const QList bookmarkKeys = + pConfig->getKeysWithGroup(QStringLiteral("[SidebarBookmarks]")); + for (const auto& bookmarkKey : bookmarkKeys) { + // qWarning() << " " << i << bookmarkKey.item << pConfig->getValueString(bookmarkKey); + const QStringList dataStrList = bookmarkKey.item.split('-', Qt::SkipEmptyParts); + if (dataStrList.size() == 1) { // root item + bool okay = false; + int fRow = dataStrList[0].toInt(&okay); + if (okay && fRow >= 0 && fRow < m_sFeatures.count()) { + toggleBookmarkByIndex(createIndex(fRow, 0, this)); + // Remove now so we don't pile up bookmarks that we removed in session + pConfig->remove(bookmarkKey); + } + } else if (dataStrList.size() == 3) { // child item + bool allInt = false; + int fRow = dataStrList[0].toInt(&allInt); + int chLevel = dataStrList[1].toInt(&allInt); + int dataType = dataStrList[2].toInt(&allInt); + if (!allInt || fRow < 0 || chLevel < 0 || (dataType != 0 && dataType != 1)) { + // invalid, ignore + continue; + } + const QString dataStr = pConfig->getValueString(bookmarkKey).trimmed(); + if (dataStr.isEmpty()) { + continue; + } + SidebarBookmark bm( + fRow, + chLevel, + 0, + QVariant(), + QString()); + if (dataType == 0) { // QVariant(int) + bool okay = false; + int dataInt = dataStr.toInt(&okay); + if (!okay) { + continue; + } + bm.data = QVariant(dataInt); + } else if (dataType == 1) { // QVariant(QSring) + bm.data = QVariant(dataStr); + } + // qWarning() << " created Bookmark" << bm; + // We now have an incomplete Bookmark (index is missing), so we + // do the same round trip like when we update bookmarks after + // the model has changed (rows added, removed) + QModelIndex bmIdx = findBookmarkIndex(bm); + if (!bmIdx.isValid()) { + // This may happen if we bookmarked a Computer folder item which + // has not been added to the tree yet due to lazy tree population + // on expand. + // Let's store the bookmark anyway so we can look it up later on + // and maybe set its index. + m_bookmarks.append(bm); + // qWarning() << " - no index found, skip"; + continue; + } + // qWarning() << " - found Bookmark index:" << bmIdx; + // qWarning() << " - index data:" << data(bmIdx, Qt::DisplayRole); + toggleBookmarkByIndex(translateChildIndex(bmIdx)); + // Remove now so we don't pile up bookmarks that we removed in session + pConfig->remove(bookmarkKey); + } else { + // invalid + continue; + } + } + return; +} diff --git a/src/library/sidebarmodel.h b/src/library/sidebarmodel.h index 15c04a8acab5..97b4dbe07c21 100644 --- a/src/library/sidebarmodel.h +++ b/src/library/sidebarmodel.h @@ -5,8 +5,110 @@ #include #include +#include "preferences/usersettings.h" + class LibraryFeature; class QTimer; +class TreeItem; + +struct SidebarBookmark { + SidebarBookmark( + int row = -1, + int cLevel = -1, + int pRow = -1, + const QVariant& datavar = QVariant(), + const QString& sLabel = QString(), + const QModelIndex& dIndex = QModelIndex()) + : featureRow(row), + childLevel(cLevel), + parentRow(pRow), + data(datavar), + label(sLabel), + index(dIndex) { + // How to? + // qWarning() << "------- created" << this; + } + bool isValid() const { + // qDebug() << " SidebarBookmark isValid()"; + // Don't validate the index. It may still be invalid, eg. when we + // read bookmarks from config + return featureRow >= 0 && + childLevel >= 0 && + parentRow >= 0 && + (data.isValid() || !label.isEmpty()); + } + bool operator==(const SidebarBookmark& other) const { + // qDebug() << " SidebarBookmark==op"; + if (featureRow != other.featureRow) { + // qDebug() << " -> featureRow != other.featureRow"; + return false; + } + if (data.isValid() && other.data.isValid()) { + // qDebug() << " -> data.isValid() && other.data.isValid()"; + // qDebug() << " -> return (data" << data << other.data; + return data == other.data; + } + // qDebug() << " -> else -> return (label" << label << other.label; + return label == other.label; + } + bool operator<(const SidebarBookmark& other) const { + if (featureRow == other.featureRow) { + if (childLevel == other.childLevel) { + return parentRow < other.parentRow; + } + return childLevel < other.childLevel; + } + return featureRow < other.featureRow; + } + + // Store feature row, child level and row number relative to first parent in + // order to allow sorting bookmarks by their position in the tree. We need + // this because QModelIndex operator<() doesn#t work for our multi-level trees. + // With the feature row we can also quickly ignore irrelevant items when + // searching for a bookmark a rebuilt child model. + int featureRow; + int childLevel; + int parentRow; + // The TreeItem data. CrateId, int playlist id or directory path / special + // node identifier in BrowseFeature. + QVariant data; + // For child items that have invalid data (Tracks Missing|Hidden and + // AutoDJ Crates) or when the LibraryFeature says the data is not unique + // (common playlist id of YEAR nodes). + QString label; + // The associated sidebar index. This is used to build the index lookup list + // for getNextPrevBookmarkIndex(). + // Must be updated when the child model is changed. + QModelIndex index; + + // TODO How to store Bookmarks in config? + // [SidebarBookmarks] + // 3 /home/user/Music/SomeAlbum + // [featureRow] [dataOrLabel] + // dataOrLabel can be either int (int for playlist or CrateId which is + // converted to QVariant(int) so I could rework/duplicate + // findBookmarkIndex() and either + // * check data type of a random item of the feature + // -> could work. check History + // * try to convert dataLabel to int, if okay + // * try to find QVariant(intValue) + // * if not okay or int search fails, try QString +}; + +inline QDebug operator<<(QDebug dbg, const SidebarBookmark& bm) { + dbg << "SidebarBookmark" << bm.featureRow << bm.childLevel << bm.parentRow; + if (bm.data.isValid()) { + if (bm.data.canConvert()) { + dbg << bm.data.toString(); + } else if (bm.data.canConvert()) { + dbg << bm.data.toInt(); + } + } + if (!bm.label.isEmpty()) { + dbg << bm.label; + } + return dbg; +} class SidebarModel : public QAbstractItemModel { Q_OBJECT @@ -17,6 +119,7 @@ class SidebarModel : public QAbstractItemModel { enum Roles { IconNameRole = Qt::UserRole + 1, + NeedsUpdateRole = Qt::UserRole + 2, DataRole, }; Q_ENUM(Roles); @@ -49,6 +152,17 @@ class SidebarModel : public QAbstractItemModel { void clear(const QModelIndex& index); void paste(const QModelIndex& index); + + void toggleBookmarkByIndex(const QModelIndex& selIndex); + QModelIndex getNextPrevBookmarkIndex(const QModelIndex& selIndex, int direction); + + void saveBookmarksToConfig(UserSettingsPointer pConfig); + void loadBookmarksFromConfig(UserSettingsPointer pConfig); + + bool indexIsBookmark(const QModelIndex& index) const; + bool indexNeedsUpdate(const QModelIndex& index) const; + void updateItem(const QModelIndex& index); + public slots: void pressed(const QModelIndex& index); void clicked(const QModelIndex& index); @@ -66,10 +180,9 @@ class SidebarModel : public QAbstractItemModel { // void slotColumnsInserted(const QModelIndex& parent, int start, int end); // void slotColumnsRemoved(const QModelIndex& parent, int start, int end); void slotDataChanged(const QModelIndex& topLeft, const QModelIndex & bottomRight); - //void slotHeaderDataChanged(Qt::Orientation orientation, int first, int last); + // void slotHeaderDataChanged(Qt::Orientation orientation, int first, int last); // void slotLayoutAboutToBeChanged(); // void slotLayoutChanged(); - // void slotModelAboutToBeReset(); // void slotModelReset(); void slotRowsAboutToBeInserted(const QModelIndex& parent, int start, int end); void slotRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end); @@ -97,7 +210,15 @@ class SidebarModel : public QAbstractItemModel { QTimer* const m_pressedUntilClickedTimer; QModelIndex m_pressedIndex; + QList m_bookmarks; + QModelIndexList m_bookmarkIndices; void startPressedUntilClickedTimer(const QModelIndex& pressedIndex); void stopPressedUntilClickedTimer(); + + QModelIndexList sortBookmarksUpdateIndices(QList* pBookmarks); + QModelIndex getBookmarkIndexByPos(int pos); + QModelIndex findBookmarkIndex(const SidebarBookmark& bookmark); + SidebarBookmark createBookmarkFromIndex(const QModelIndex& index); + void maybeUpdateBookmarkIndices(const QModelIndex& index); }; diff --git a/src/library/trackset/baseplaylistfeature.h b/src/library/trackset/baseplaylistfeature.h index 933c391af32d..e859bf0f8711 100644 --- a/src/library/trackset/baseplaylistfeature.h +++ b/src/library/trackset/baseplaylistfeature.h @@ -44,13 +44,13 @@ class BasePlaylistFeature : public BaseTrackSetFeature { virtual void activatePlaylist(int playlistId); virtual void htmlLinkClicked(const QUrl& link); - virtual void slotPlaylistTableChanged(int playlistId) = 0; - void slotPlaylistTableChangedAndSelect(int playlistId) { - slotPlaylistTableChanged(playlistId); - selectPlaylistInSidebar(playlistId); - }; - void slotPlaylistTableChangedAndScrollTo(int playlistId) { - slotPlaylistTableChanged(playlistId); + virtual void slotPlaylistTableChanged( + int playlistId, + PlaylistDAO::HiddenType type) = 0; + void slotPlaylistTableChangedAndScrollTo( + int playlistId, + PlaylistDAO::HiddenType type) { + slotPlaylistTableChanged(playlistId, type); selectPlaylistInSidebar(playlistId, false); }; virtual void slotPlaylistContentOrLockChanged(const QSet& playlistIds) = 0; diff --git a/src/library/trackset/playlistfeature.cpp b/src/library/trackset/playlistfeature.cpp index 0d7e4945622f..036ed06fe7c6 100644 --- a/src/library/trackset/playlistfeature.cpp +++ b/src/library/trackset/playlistfeature.cpp @@ -384,11 +384,9 @@ void PlaylistFeature::decorateChild(TreeItem* item, int playlistId) { } } -void PlaylistFeature::slotPlaylistTableChanged(int playlistId) { - // qDebug() << "PlaylistFeature::slotPlaylistTableChanged() playlistId:" << playlistId; - enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId); - if (type != PlaylistDAO::PLHT_NOT_HIDDEN && // not a regular playlist - type != PlaylistDAO::PLHT_UNKNOWN) { // not a deleted playlist +void PlaylistFeature::slotPlaylistTableChanged(int playlistId, PlaylistDAO::HiddenType type) { + // qDebug() << "PlaylistFeature::slotPlaylistTableChanged() playlistId:" << playlistId << type; + if (type != PlaylistDAO::PLHT_NOT_HIDDEN) { // not a regular playlist return; } @@ -436,7 +434,7 @@ void PlaylistFeature::slotPlaylistTableRenamed(int playlistId, const QString& ne if (m_playlistDao.getHiddenType(playlistId) == PlaylistDAO::PLHT_NOT_HIDDEN) { // Maybe we need to re-sort the sidebar items, so call slotPlaylistTableChanged() // in order to rebuild the model, not just updateChildModel() - slotPlaylistTableChanged(playlistId); + slotPlaylistTableChanged(playlistId, PlaylistDAO::PLHT_NOT_HIDDEN); } } diff --git a/src/library/trackset/playlistfeature.h b/src/library/trackset/playlistfeature.h index 1a7772766bc3..17b16c6a0c37 100644 --- a/src/library/trackset/playlistfeature.h +++ b/src/library/trackset/playlistfeature.h @@ -33,7 +33,7 @@ class PlaylistFeature : public BasePlaylistFeature { void onRightClickChild(const QPoint& globalPos, const QModelIndex& index) override; private slots: - void slotPlaylistTableChanged(int playlistId) override; + void slotPlaylistTableChanged(int playlistId, PlaylistDAO::HiddenType type) override; void slotPlaylistContentOrLockChanged(const QSet& playlistIds) override; void slotPlaylistTableRenamed(int playlistId, const QString& newName) override; void slotShufflePlaylist(); diff --git a/src/library/trackset/setlogfeature.cpp b/src/library/trackset/setlogfeature.cpp index a548c075e926..7afccb5c7f04 100644 --- a/src/library/trackset/setlogfeature.cpp +++ b/src/library/trackset/setlogfeature.cpp @@ -164,15 +164,12 @@ void SetlogFeature::onRightClick(const QPoint& globalPos) { Q_UNUSED(globalPos); m_lastRightClickedIndex = QModelIndex(); - // Create the right-click menu - // QMenu menu(NULL); - // menu.addAction(m_pCreatePlaylistAction); + // There is no action associated with the root item // TODO(DASCHUER) add something like disable logging - // menu.exec(globalPos); } void SetlogFeature::onRightClickChild(const QPoint& globalPos, const QModelIndex& index) { - //Save the model index so we can get it in the action slots... + // Save the model index so we can get it in the action slots... m_lastRightClickedIndex = index; int playlistId = playlistIdFromIndex(index); @@ -253,7 +250,6 @@ QModelIndex SetlogFeature::constructChildModel(int selectedId) { " GROUP BY Playlists.id") .arg(m_countsDurationTableName, QString::number(PlaylistDAO::PLHT_SET_LOG)); - ; queryString.append( mixxx::DbConnection::collateLexicographically( " ORDER BY sort_name")); @@ -359,14 +355,11 @@ void SetlogFeature::decorateChild(TreeItem* item, int playlistId) { /// Invoked on startup to create new current playlist and by "Finish current and start new" void SetlogFeature::slotGetNewPlaylist() { - //qDebug() << "slotGetNewPlaylist() successfully triggered !"; + // qDebug() << "slotGetNewPlaylist() successfully triggered !"; // create a new playlist for today - QString set_log_name_format; - QString set_log_name; - - set_log_name = QDate::currentDate().toString(Qt::ISODate); - set_log_name_format = set_log_name + " #%1"; + QString set_log_name = QDate::currentDate().toString(Qt::ISODate); + QString set_log_name_format = set_log_name + " #%1"; int i = 1; // calculate name of the todays setlog @@ -374,7 +367,8 @@ void SetlogFeature::slotGetNewPlaylist() { set_log_name = set_log_name_format.arg(++i); } - //qDebug() << "Creating session history playlist name:" << set_log_name; + // qDebug() << "Creating session history playlist name:" << set_log_name; + int previousPlaylistid = m_currentPlaylistId; m_currentPlaylistId = m_playlistDao.createPlaylist( set_log_name, PlaylistDAO::PLHT_SET_LOG); @@ -387,10 +381,10 @@ void SetlogFeature::slotGetNewPlaylist() { m_playlistDao.setCurrentHistoryPlaylistId(m_currentPlaylistId); } - // reload child model again because the 'added' signal fired by PlaylistDAO - // might have triggered slotPlaylistTableChanged() before m_currentPlaylistId was set, - // which causes the wrong playlist being decorated as 'current' - slotPlaylistTableChanged(m_currentPlaylistId); + // Update child model again because the 'added' signal fired by PlaylistDAO + // might have triggered slotPlaylistTableChanged() before m_currentPlaylistId + // was set, which causes the wrong playlist being decorated as 'current'. + slotPlaylistContentOrLockChanged(QSet{previousPlaylistid, m_currentPlaylistId}); } void SetlogFeature::slotJoinWithPrevious() { @@ -508,9 +502,11 @@ void SetlogFeature::lockOrUnlockAllChildPlaylists(bool lock) { return; } if (lock) { - qWarning() << "lock all child playlists of" << m_lastRightClickedIndex.data().toString(); + qDebug() << "SetlogFeature: locking all child playlists of" + << m_lastRightClickedIndex.data().toString(); } else { - qWarning() << "unlock all child playlists of" << m_lastRightClickedIndex.data().toString(); + qWarning() << "SetlogFeature: unlocking all child playlists of" + << m_lastRightClickedIndex.data().toString(); } TreeItem* item = static_cast(m_lastRightClickedIndex.internalPointer()); if (!item) { @@ -663,11 +659,12 @@ void SetlogFeature::slotPlayingTrackChanged(TrackPointer currentPlayingTrack) { } } -void SetlogFeature::slotPlaylistTableChanged(int playlistId) { - // qDebug() << "SetlogFeature::slotPlaylistTableChanged() id:" << playlistId; - PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId); - if (type != PlaylistDAO::PLHT_SET_LOG && - type != PlaylistDAO::PLHT_UNKNOWN) { // deleted Playlist +void SetlogFeature::slotPlaylistTableChanged(int playlistId, PlaylistDAO::HiddenType type) { + // qDebug() << "SetlogFeature::slotPlaylistTableChanged() id:" << playlistId << type; + // Note: we only care about PLHT_SET_LOG as that's the only relevant type for + // rebuilding the tree. Type PLHT_UNKNOWN is only used for YEAR placeholder + // playlist and is assigned to items dynamically with YEAR label. + if (type != PlaylistDAO::PLHT_SET_LOG) { return; } @@ -682,7 +679,7 @@ void SetlogFeature::slotPlaylistTableChanged(int playlistId) { // a YEAR item was selected selectedYearIndexRow = m_lastClickedIndex.row(); } else if (playlistId == lastClickedPlaylistId && - type == PlaylistDAO::PLHT_UNKNOWN) { + m_playlistDao.getHiddenType(lastClickedPlaylistId) == PlaylistDAO::PLHT_UNKNOWN) { // selected playlist was deleted, find a sibling. // prev/next works here because history playlists are always // sorted by date of creation. diff --git a/src/library/trackset/setlogfeature.h b/src/library/trackset/setlogfeature.h index 143641738717..4d0980a0bb8b 100644 --- a/src/library/trackset/setlogfeature.h +++ b/src/library/trackset/setlogfeature.h @@ -21,6 +21,9 @@ class SetlogFeature : public BasePlaylistFeature { void bindLibraryWidget(WLibrary* libraryWidget, KeyboardEventFilter* keyboard) override; void activatePlaylist(int playlistId) override; + bool isItemDataUnique(const QVariant& data) const override { + return data != QVariant(m_yearNodeId); + } public slots: void onRightClick(const QPoint& globalPos) override; @@ -40,7 +43,9 @@ class SetlogFeature : public BasePlaylistFeature { private slots: void slotPlayingTrackChanged(TrackPointer currentPlayingTrack); - void slotPlaylistTableChanged(int playlistId) override; + void slotPlaylistTableChanged( + int playlistId, + PlaylistDAO::HiddenType type) override; void slotPlaylistContentOrLockChanged(const QSet& playlistIds) override; void slotPlaylistTableRenamed(int playlistId, const QString& newName) override; void slotDeleteAllUnlockedChildPlaylists(); diff --git a/src/library/treeitem.cpp b/src/library/treeitem.cpp index a6bd742005a1..9e29a86907c8 100644 --- a/src/library/treeitem.cpp +++ b/src/library/treeitem.cpp @@ -1,5 +1,6 @@ #include "library/treeitem.h" +#include "library/libraryfeature.h" #include "util/make_const_iterator.h" /* @@ -31,11 +32,12 @@ TreeItem::TreeItem( LibraryFeature* pFeature, QString label, QVariant data) - : m_pFeature(pFeature), - m_pParent(nullptr), - m_label(std::move(label)), - m_data(std::move(data)), - m_bold(false) { + : m_pFeature(pFeature), + m_pParent(nullptr), + m_label(std::move(label)), + m_data(std::move(data)), + m_bold(false), + m_needsUpdate(false) { } TreeItem::~TreeItem() { @@ -110,3 +112,7 @@ void TreeItem::removeChildren(int row, int count) { qDeleteAll(m_children.constBegin() + row, m_children.constBegin() + (row + count)); constErase(&m_children, m_children.constBegin() + row, m_children.constBegin() + (row + count)); } + +bool TreeItem::isDataUniqueInFeature() const { + return feature()->isItemDataUnique(m_data); +} diff --git a/src/library/treeitem.h b/src/library/treeitem.h index df80de26b26f..7d92ccaa63a6 100644 --- a/src/library/treeitem.h +++ b/src/library/treeitem.h @@ -83,6 +83,16 @@ class TreeItem final { const QList& children() const { return m_children; } + /// Get the tree level of the item. Returns 0 if this is the root. + int childLevel() const { + int level = 0; + TreeItem* pTempItem = const_cast(this); + while (pTempItem->hasParent()) { + level++; + pTempItem = pTempItem->parent(); + } + return level; + } TreeItem* appendChild( QString label, @@ -111,6 +121,7 @@ class TreeItem final { const QVariant& getData() const { return m_data; } + bool isDataUniqueInFeature() const; void setIcon(const QIcon& icon) { m_icon = icon; @@ -125,6 +136,12 @@ class TreeItem final { bool isBold() const { return m_bold; } + void setNeedsUpdate(bool needsUpdate) { + m_needsUpdate = needsUpdate; + } + bool needsUpdate() { + return m_needsUpdate; + } private: explicit TreeItem( @@ -135,8 +152,9 @@ class TreeItem final { void initFeatureRecursively(LibraryFeature* pFeature); // The library feature is inherited from the parent. - // For all child items this is just a shortcut to the - // library feature of the root item! + // For all child items this is done via + // insertChildren() -> initFeatureRecursively() and is just + // a shortcut to the library feature of the root item! LibraryFeature* m_pFeature; TreeItem* m_pParent; @@ -147,4 +165,5 @@ class TreeItem final { QVariant m_data; QIcon m_icon; bool m_bold; + bool m_needsUpdate; }; diff --git a/src/library/treeitemmodel.cpp b/src/library/treeitemmodel.cpp index 36e40096fdc6..8d1aa9d7b798 100644 --- a/src/library/treeitemmodel.cpp +++ b/src/library/treeitemmodel.cpp @@ -11,7 +11,7 @@ // 1. argument represents a name shown in the sidebar view later on // 2. argument represents the absolute path of this tree item // 3. argument is a library feature object. -// This is necessary because in sidebar.cpp we handle 'activateChid' events +// This is necessary because in wlibrarysidebar.cpp we handle 'activateChild' events // 4. the parent TreeItem object // The constructor does not add this TreeItem object to the parent's child list // @@ -45,16 +45,16 @@ QVariant TreeItemModel::data(const QModelIndex &index, int role) const { return QVariant(); } - TreeItem *item = static_cast(index.internalPointer()); + TreeItem* pItem = static_cast(index.internalPointer()); // We use Qt::UserRole to ask for the data. switch (role) { case Qt::DisplayRole: - return item->getLabel(); + return pItem->getLabel(); case kDataRole: - return item->getData(); + return pItem->getData(); case kBoldRole: - return item->isBold(); + return pItem->isBold(); default: return QVariant(); } @@ -63,7 +63,7 @@ QVariant TreeItemModel::data(const QModelIndex &index, int role) const { bool TreeItemModel::setData(const QModelIndex &a_rIndex, const QVariant &a_rValue, int a_iRole) { // Get the item referred to by this index. - TreeItem *pItem = static_cast(a_rIndex.internalPointer()); + TreeItem* pItem = static_cast(a_rIndex.internalPointer()); if (pItem == nullptr) { return false; } @@ -107,16 +107,16 @@ QModelIndex TreeItemModel::index(int row, int column, const QModelIndex &parent) return QModelIndex(); } - TreeItem *parentItem; + TreeItem* pParentItem; if (parent.isValid()) { - parentItem = static_cast(parent.internalPointer()); + pParentItem = static_cast(parent.internalPointer()); } else { - parentItem = getRootItem(); + pParentItem = getRootItem(); } - TreeItem *childItem = parentItem->child(row); - if (childItem) { - return createIndex(row, column, childItem); + TreeItem* pChildItem = pParentItem->child(row); + if (pChildItem) { + return createIndex(row, column, pChildItem); } else { return QModelIndex(); } @@ -127,14 +127,14 @@ QModelIndex TreeItemModel::parent(const QModelIndex& index) const { return QModelIndex(); } - TreeItem *childItem = static_cast(index.internalPointer()); - TreeItem *parentItem = childItem->parent(); - if (!parentItem) { + TreeItem* pChildItem = static_cast(index.internalPointer()); + TreeItem* pParentItem = pChildItem->parent(); + if (!pParentItem) { return QModelIndex(); - } else if (parentItem == getRootItem()) { + } else if (pParentItem == getRootItem()) { return createIndex(0, 0, getRootItem()); } else { - return createIndex(parentItem->parentRow(), 0, parentItem); + return createIndex(pParentItem->parentRow(), 0, pParentItem); } } @@ -190,10 +190,10 @@ bool TreeItemModel::removeRows(int position, int rows, const QModelIndex &parent if (rows == 0) { return true; } - TreeItem *parentItem = getItem(parent); + TreeItem* pParentItem = getItem(parent); beginRemoveRows(parent, position, position + rows - 1); - parentItem->removeChildren(position, rows); + pParentItem->removeChildren(position, rows); endRemoveRows(); return true; diff --git a/src/widget/wlibrarysidebar.cpp b/src/widget/wlibrarysidebar.cpp index ebd74d94d178..4cd05970d3b2 100644 --- a/src/widget/wlibrarysidebar.cpp +++ b/src/widget/wlibrarysidebar.cpp @@ -5,16 +5,26 @@ #include #include "library/library_prefs.h" +#include "library/sidebaritemdelegate.h" #include "library/sidebarmodel.h" #include "moc_wlibrarysidebar.cpp" #include "util/defs.h" #include "util/dnd.h" +namespace { + +const QColor kDefaultBookmarkColor = QColor(Qt::red); + +} // anonymous namespace + WLibrarySidebar::WLibrarySidebar(QWidget* parent) : QTreeView(parent), WBaseWidget(this), + m_pSidebarModel(nullptr), + m_pItemDelegate(nullptr), m_hoverExpandDelay(mixxx::library::prefs::kSidebarHoverExpandDelayDefault), - m_lastDragMoveAccepted(false) { + m_lastDragMoveAccepted(false), + m_bookmarkColor(kDefaultBookmarkColor) { qRegisterMetaType("FocusWidget"); //Set some properties setHeaderHidden(true); @@ -31,6 +41,24 @@ WLibrarySidebar::WLibrarySidebar(QWidget* parent) header()->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); } +void WLibrarySidebar::setModel(QAbstractItemModel* pModel) { + SidebarModel* pSidebarModel = qobject_cast(pModel); + DEBUG_ASSERT(pSidebarModel); + m_pSidebarModel = pSidebarModel; + QTreeView::setModel(pSidebarModel); + // Create the delegate for painting the bookmark indicator + DEBUG_ASSERT(m_pItemDelegate == nullptr); + m_pItemDelegate = new SidebarItemDelegate(this, pSidebarModel); + setItemDelegateForColumn(0, m_pItemDelegate); + m_pItemDelegate->setBookmarkColor(m_bookmarkColor); + // Color can be set in qss via qproperty-bookmarkColor which happens + // when the stylesheet is applied. Push it to delegate. + connect(this, + &WLibrarySidebar::bookmarkColorChanged, + m_pItemDelegate, + &SidebarItemDelegate::setBookmarkColor); +} + void WLibrarySidebar::contextMenuEvent(QContextMenuEvent* pEvent) { // if (pEvent->state() & Qt::RightButton) { //Dis shiz don werk on windowze QModelIndex clickedIndex = indexAt(pEvent->pos()); @@ -116,13 +144,7 @@ void WLibrarySidebar::dragMoveEvent(QDragMoveEvent* pEvent) { return; } - SidebarModel* pSidebarModel = qobject_cast(model()); - VERIFY_OR_DEBUG_ASSERT(pSidebarModel) { - m_lastDragMoveAccepted = false; - pEvent->ignore(); - return; - } - if (pSidebarModel->dragMoveAccept(index, urls)) { + if (m_pSidebarModel->dragMoveAccept(index, urls)) { m_lastDragMoveAccepted = true; pEvent->acceptProposedAction(); } else { @@ -166,11 +188,6 @@ void WLibrarySidebar::dropEvent(QDropEvent* pEvent) { // track table widget onto the sidebar. // Reset the selected items (if you had anything highlighted, it clears it) // this->selectionModel()->clear(); - SidebarModel* pSidebarModel = qobject_cast(model()); - VERIFY_OR_DEBUG_ASSERT(pSidebarModel) { - pEvent->ignore(); - return; - } #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QPoint pos = pEvent->position().toPoint(); #else @@ -181,7 +198,7 @@ void WLibrarySidebar::dropEvent(QDropEvent* pEvent) { // pEvent->source() will return NULL if something is dropped from // a different application const QList urls = pEvent->mimeData()->urls(); - if (pSidebarModel->dropAccept(destIndex, urls, pEvent->source())) { + if (m_pSidebarModel->dropAccept(destIndex, urls, pEvent->source())) { pEvent->acceptProposedAction(); } else { pEvent->ignore(); @@ -216,41 +233,44 @@ void WLibrarySidebar::renameSelectedItem() { } void WLibrarySidebar::toggleSelectedItem() { - QModelIndex index = selectedIndex(); - if (index.isValid()) { - // Activate the item so its content shows in the main library. - emit clicked(index); - // Expand or collapse the item as necessary. - setExpanded(index, !isExpanded(index)); + const QModelIndex index = selectedIndex(); + if (!index.isValid()) { + return; } + // Activate the item so its content shows in the main library. + emit clicked(index); + // Update child tree of BrowseFeature items with outdated tree + if (m_pSidebarModel->indexNeedsUpdate(index)) { + m_pSidebarModel->updateItem(index); + return; + } + // Expand or collapse the item as necessary. + setExpanded(index, !isExpanded(index)); } bool WLibrarySidebar::isLeafNodeSelected() { - QModelIndex index = selectedIndex(); - if (index.isValid()) { - if(!index.model()->hasChildren(index)) { - return true; - } - const SidebarModel* pSidebarModel = qobject_cast(index.model()); - if (pSidebarModel) { - return pSidebarModel->hasTrackTable(index); - } + const QModelIndex index = selectedIndex(); + if (!index.isValid()) { + return false; + } + + if (!index.model()->hasChildren(index)) { + return true; + } + const SidebarModel* pSidebarModel = qobject_cast(index.model()); + if (pSidebarModel) { + return pSidebarModel->hasTrackTable(index); } return false; } bool WLibrarySidebar::isChildIndexSelected(const QModelIndex& index) { // qDebug() << "WLibrarySidebar::isChildIndexSelected" << index; - QModelIndex selIndex = selectedIndex(); + const QModelIndex selIndex = selectedIndex(); if (!selIndex.isValid()) { return false; } - SidebarModel* pSidebarModel = qobject_cast(model()); - VERIFY_OR_DEBUG_ASSERT(pSidebarModel) { - // qDebug() << " >> model() is not SidebarModel"; - return false; - } - QModelIndex translated = pSidebarModel->translateChildIndex(index); + const QModelIndex translated = m_pSidebarModel->translateChildIndex(index); if (!translated.isValid()) { // qDebug() << " >> index can't be translated"; return false; @@ -260,35 +280,94 @@ bool WLibrarySidebar::isChildIndexSelected(const QModelIndex& index) { bool WLibrarySidebar::isFeatureRootIndexSelected(LibraryFeature* pFeature) { // qDebug() << "WLibrarySidebar::isFeatureRootIndexSelected"; - QModelIndex selIndex = selectedIndex(); + const QModelIndex selIndex = selectedIndex(); if (!selIndex.isValid()) { return false; } - SidebarModel* pSidebarModel = qobject_cast(model()); - VERIFY_OR_DEBUG_ASSERT(pSidebarModel) { - return false; - } - const QModelIndex rootIndex = pSidebarModel->getFeatureRootIndex(pFeature); + const QModelIndex rootIndex = m_pSidebarModel->getFeatureRootIndex(pFeature); return rootIndex == selIndex; } +void WLibrarySidebar::setBookmarkColor(const QColor& color) { + if (color.isValid() && m_pItemDelegate) { + m_pItemDelegate->setBookmarkColor(color); + } +} + +void WLibrarySidebar::toggleBookmark() { + const QModelIndex selIndex = selectedIndex(); + if (!selIndex.isValid()) { + qWarning() << " ! WLS bookmarkSelectedItem, invalid index" << selIndex; + return; + } + + m_pSidebarModel->toggleBookmarkByIndex(selIndex); + update(); +} + +void WLibrarySidebar::slotGoToNextPrevBookmark(int direction) { + // Don't use selectedIndex(). Selected item may not be the focused item, eg. + // if we focused a bookmark item without activating it. + QModelIndex index = currentIndex(); + if (!index.isValid()) { + qDebug() << "WLibrarySidebar::goToNextPrevBookmark invalid index" << index; + return; + } + + const QModelIndex bookmarkIdx = m_pSidebarModel->getNextPrevBookmarkIndex(index, direction); + if (!bookmarkIdx.isValid() || bookmarkIdx == index) { + // No bookmarks stored or none of them has been found. + // Or, we are already on the only bookmark. In that case we don't reselect because + // that would cause resorting (and reloading tracks for Computer path indices). + return; + } + + // just scroll to and highlight (focus) + // Note: scrollTo() with default hint EnsureVisible will also expand all + // parents, which in turn emits expanded() for each index which invokes + // LibraryFeature::onLazyChildExpandation(). + scrollTo(bookmarkIdx); + // Use this instead of setCurrentIndex() to keep current selection + selectionModel()->setCurrentIndex(bookmarkIdx, QItemSelectionModel::NoUpdate); + // TODO add control [Library],goToSelectedItem ?? + // Or add wrapper goToItem() that does + // * select & activate focused item if it's not selected + // * expand / collapse + // * jump to tracks if double-tapped +} + /// Invoked by actual keypresses (requires widget focus) and emulated keypresses /// sent by LibraryControl void WLibrarySidebar::keyPressEvent(QKeyEvent* pEvent) { // TODO(XXX) Should first keyEvent ensure previous item has focus? I.e. if the selected // item is not focused, require second press to perform the desired action. - - SidebarModel* pSidebarModel = qobject_cast(model()); - QModelIndex selIndex = selectedIndex(); - if (pSidebarModel && selIndex.isValid() && pEvent->matches(QKeySequence::Paste)) { - pSidebarModel->paste(selIndex); + const QModelIndex selIndex = selectedIndex(); + if (selIndex.isValid() && pEvent->matches(QKeySequence::Paste)) { + m_pSidebarModel->paste(selIndex); return; } - focusSelectedIndex(); + // Don't focus selection if we receive a modifier-only event, for example + // Alt + B: un/bookmark selected item + // Alt + Up/Down: jump to and highlight next/previous bookmarked item + // Press Enter to activate + if (pEvent->modifiers().testFlag(Qt::AltModifier)) { + if (pEvent->key() == Qt::Key_Down || pEvent->key() == Qt::Key_Up) { + slotGoToNextPrevBookmark(pEvent->key() == Qt::Key_Down ? 1 : -1); + } else if (pEvent->key() == Qt::Key_B) { + toggleBookmark(); + } + // No further Alt, might as well be a system shortcut + return; + } switch (pEvent->key()) { case Qt::Key_Return: + // If the selection is not focused, focus it and scroll to it first. + // Happens when going to bookmark with activating it. + if (selectFocusedIndex()) { + return; + } toggleSelectedItem(); return; case Qt::Key_Down: @@ -297,6 +376,11 @@ void WLibrarySidebar::keyPressEvent(QKeyEvent* pEvent) { case Qt::Key_PageUp: case Qt::Key_End: case Qt::Key_Home: { + // If the selection is not focused, focus it and scroll to it first. + // Happens when going to bookmark without activating it. + if (focusSelectedIndex()) { + return; + } // Let the tree view move up and down for us. QTreeView::keyPressEvent(pEvent); // After the selection changed force-activate (click) the newly selected @@ -316,11 +400,17 @@ void WLibrarySidebar::keyPressEvent(QKeyEvent* pEvent) { if (pEvent->modifiers() & Qt::ControlModifier) { emit setLibraryFocus(FocusWidget::TracksTable); } else { + if (focusSelectedIndex()) { + return; + } QTreeView::keyPressEvent(pEvent); } return; } case Qt::Key_Left: { + if (focusSelectedIndex()) { + return; + } // If an expanded item is selected let QTreeView collapse it QModelIndex selIndex = selectedIndex(); if (!selIndex.isValid()) { @@ -374,7 +464,7 @@ void WLibrarySidebar::mousePressEvent(QMouseEvent* pEvent) { void WLibrarySidebar::focusInEvent(QFocusEvent* pEvent) { // Clear the current index, i.e. remove the focus indicator - selectionModel()->clearCurrentIndex(); + focusSelectedIndex(); QTreeView::focusInEvent(pEvent); } @@ -388,9 +478,6 @@ void WLibrarySidebar::selectIndex(const QModelIndex& index, bool scrollToIndex) if (selectionModel()) { selectionModel()->deleteLater(); } - if (index.parent().isValid()) { - expand(index.parent()); - } setSelectionModel(pModel); if (!scrollToIndex) { // With auto-scroll enabled, setCurrentIndex() would scroll there. @@ -400,6 +487,9 @@ void WLibrarySidebar::selectIndex(const QModelIndex& index, bool scrollToIndex) } setCurrentIndex(index); if (scrollToIndex) { + // Note: scrollTo() with default hint EnsureVisible will also expand all + // parents, which in turn emits expanded() for each index which invokes + // LibraryFeature::onLazyChildExpandation(). scrollTo(index); } else { setAutoScroll(true); @@ -408,18 +498,13 @@ void WLibrarySidebar::selectIndex(const QModelIndex& index, bool scrollToIndex) /// Selects a child index from a feature and ensures visibility void WLibrarySidebar::selectChildIndex(const QModelIndex& index, bool selectItem) { - SidebarModel* pSidebarModel = qobject_cast(model()); - VERIFY_OR_DEBUG_ASSERT(pSidebarModel) { - qDebug() << "model() is not SidebarModel"; - return; - } - QModelIndex translated = pSidebarModel->translateChildIndex(index); + const QModelIndex translated = m_pSidebarModel->translateChildIndex(index); if (!translated.isValid()) { return; } if (selectItem) { - auto* pModel = new QItemSelectionModel(pSidebarModel); + auto* pModel = new QItemSelectionModel(m_pSidebarModel); pModel->select(translated, QItemSelectionModel::Select); if (selectionModel()) { selectionModel()->deleteLater(); @@ -428,12 +513,10 @@ void WLibrarySidebar::selectChildIndex(const QModelIndex& index, bool selectItem setCurrentIndex(translated); } - QModelIndex parentIndex = translated.parent(); - while (parentIndex.isValid()) { - expand(parentIndex); - parentIndex = parentIndex.parent(); - } - scrollTo(translated, EnsureVisible); + // Note: scrollTo() with default hint EnsureVisible will also expand all + // parents, which in turn emits expanded() for each index which invokes + // LibraryFeature::onLazyChildExpandation(). + scrollTo(translated); } QModelIndex WLibrarySidebar::selectedIndex() { @@ -447,14 +530,32 @@ QModelIndex WLibrarySidebar::selectedIndex() { } /// Refocus the selected item after right-click -void WLibrarySidebar::focusSelectedIndex() { +bool WLibrarySidebar::focusSelectedIndex() { // After the context menu was activated (and closed, with or without clicking // an action), the currentIndex is the right-clicked item. // If if the currentIndex is not selected, make the selection the currentIndex QModelIndex selIndex = selectedIndex(); if (selIndex.isValid() && selIndex != selectionModel()->currentIndex()) { setCurrentIndex(selIndex); + return true; } + return false; +} + +bool WLibrarySidebar::selectFocusedIndex() { + const QModelIndex selIndex = selectedIndex(); + const QModelIndex focusIndex = selectionModel()->currentIndex(); + // qDebug() << " -- selected index:" << selIndex; + // qDebug() << " -- focused index: " << focusIndex; + if (focusIndex.isValid() && focusIndex != selIndex) { + // qDebug() << " -- select focused index, scroll to"; + scrollTo(focusIndex); + selectIndex(focusIndex); + emit pressed(focusIndex); + return true; + } + // qDebug() << " -- ! focused index invalid" << focusIndex; + return false; } bool WLibrarySidebar::event(QEvent* pEvent) { diff --git a/src/widget/wlibrarysidebar.h b/src/widget/wlibrarysidebar.h index 55e0baaf6892..f150620e27f8 100644 --- a/src/widget/wlibrarysidebar.h +++ b/src/widget/wlibrarysidebar.h @@ -8,6 +8,8 @@ #include "widget/wbasewidget.h" class LibraryFeature; +class SidebarItemDelegate; +class SidebarModel; class QPoint; class WLibrarySidebar : public QTreeView, public WBaseWidget { @@ -15,6 +17,13 @@ class WLibrarySidebar : public QTreeView, public WBaseWidget { public: explicit WLibrarySidebar(QWidget* parent = nullptr); + Q_PROPERTY(QColor bookmarkColor + MEMBER m_bookmarkColor + NOTIFY bookmarkColorChanged + DESIGNABLE true); + + void setModel(QAbstractItemModel* pModel) override; + void contextMenuEvent(QContextMenuEvent* pEvent) override; void dragMoveEvent(QDragMoveEvent* pEvent) override; void dragEnterEvent(QDragEnterEvent* pEvent) override; @@ -30,11 +39,14 @@ class WLibrarySidebar : public QTreeView, public WBaseWidget { bool isChildIndexSelected(const QModelIndex& index); bool isFeatureRootIndexSelected(LibraryFeature* pFeature); + void setBookmarkColor(const QColor& color); + public slots: void selectIndex(const QModelIndex& index, bool scrollToIndex = true); void selectChildIndex(const QModelIndex&, bool selectItem = true); void slotSetFont(const QFont& font); void slotSetExpandOnHoverDelay(int delay); + void slotGoToNextPrevBookmark(int direction); signals: void rightClicked(const QPoint&, const QModelIndex&); @@ -42,19 +54,25 @@ class WLibrarySidebar : public QTreeView, public WBaseWidget { void deleteItem(const QModelIndex&); FocusWidget setLibraryFocus(FocusWidget newFocus, Qt::FocusReason focusReason = Qt::OtherFocusReason); + void bookmarkColorChanged(QColor m_bookmarkColor); protected: bool event(QEvent* pEvent) override; private: - void focusSelectedIndex(); + bool focusSelectedIndex(); + bool selectFocusedIndex(); QModelIndex selectedIndex(); void toggleDragHoverPropertyAndUpdateStyle(bool enabled); void resetHoverIndexAndDragMoveResult(); + void toggleBookmark(); + SidebarModel* m_pSidebarModel; + SidebarItemDelegate* m_pItemDelegate; QBasicTimer m_expandTimer; int m_hoverExpandDelay; QModelIndex m_hoverIndex; bool m_lastDragMoveAccepted; + QColor m_bookmarkColor; };