Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 83 additions & 3 deletions src/library/library.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ namespace {

const mixxx::Logger kLogger("Library");

const ConfigKey kLastSelectedTrackIdConfigKey =
ConfigKey(QStringLiteral("[Library]"), QStringLiteral("last_selected_track_id"));

} // namespace

using namespace mixxx::library::prefs;
Expand All @@ -69,7 +72,7 @@ Library::Library(
m_pConfig(pConfig),
m_pDbConnectionPool(std::move(pDbConnectionPool)),
m_pTrackCollectionManager(pTrackCollectionManager),
m_pSidebarModel(make_parented<SidebarModel>(this)),
m_pSidebarModel(make_parented<SidebarModel>(pConfig, this)),
m_pLibraryControl(make_parented<LibraryControl>(this)),
m_pLibraryWidget(nullptr),
m_pKeyNotation(std::make_unique<ControlObject>(
Expand All @@ -81,6 +84,12 @@ Library::Library(
this,
&Library::slotRefreshLibraryModels);

// Save the selected track ID on exit so the latest selection is persisted
connect(QCoreApplication::instance(),
&QCoreApplication::aboutToQuit,
this,
&Library::slotSaveSelectedTrackId);

// TODO(rryan) -- turn this construction / adding of features into a static
// method or something -- CreateDefaultLibrary
m_pMixxxLibraryFeature = make_parented<MixxxLibraryFeature>(
Expand Down Expand Up @@ -366,6 +375,10 @@ void Library::bindSidebarWidget(WLibrarySidebar* pSidebarWidget) {
&WLibrarySidebar::expanded,
m_pSidebarModel,
&SidebarModel::doubleClicked);
connect(m_pSidebarModel,
&SidebarModel::selectionSaved,
this,
&Library::slotSaveSelectedTrackId);

connect(pSidebarWidget,
&WLibrarySidebar::rightClicked,
Expand Down Expand Up @@ -612,8 +625,16 @@ void Library::slotCreateCrate() {
}

void Library::onSkinLoadFinished() {
// Enable the default selection when a new skin is loaded.
m_pSidebarModel->activateDefaultSelection();
// Try to restore last selection, fall back to default if not found
if (!m_pSidebarModel->restoreLastSelection()) {
// Enable the default selection when a new skin is loaded.
m_pSidebarModel->activateDefaultSelection();
}

// Restore the selected track after the track model has had time to load.
// The track model populates asynchronously after activateChild is called,
// and a model reset clears the selection. We retry with increasing delays.
QTimer::singleShot(1000, this, &Library::slotRestoreSelectedTrackId);
}

bool Library::requestAddDir(const QString& dir) {
Expand Down Expand Up @@ -822,3 +843,62 @@ LibraryTableModel* Library::trackTableModel() const {

return m_pMixxxLibraryFeature->trackTableModel();
}

void Library::slotSaveSelectedTrackId() {
if (!m_pConfig || !m_pLibraryWidget) {
return;
}
WTrackTableView* pView = m_pLibraryWidget->getCurrentTrackTableView();
if (pView) {
TrackId trackId = pView->getCurrentTrackId();
if (trackId.isValid()) {
m_pConfig->set(kLastSelectedTrackIdConfigKey,
ConfigValue(trackId.toVariant().toString()));
} else {
m_pConfig->set(kLastSelectedTrackIdConfigKey, ConfigValue());
}
}
}

void Library::slotRestoreSelectedTrackId() {
if (!m_pConfig || !m_pLibraryWidget) {
return;
}
QString trackIdStr = m_pConfig->getValue(kLastSelectedTrackIdConfigKey);
if (trackIdStr.isEmpty()) {
return;
}
TrackId trackId{QVariant(trackIdStr)};
if (!trackId.isValid()) {
return;
}

// setCurrentTrackId calls selectRow then setCurrentIndex with SelectCurrent,
// which clears the row selection. Re-select the row after it succeeds.
auto selectAndReselect = [](WTrackTableView* pView, const TrackId& id) {
if (pView->setCurrentTrackId(id, 0, true)) {
QModelIndex idx = pView->currentIndex();
if (idx.isValid()) {
pView->selectRow(idx.row());
}
return true;
}
return false;
};

WTrackTableView* pView = m_pLibraryWidget->getCurrentTrackTableView();
if (pView) {
if (!selectAndReselect(pView, trackId)) {
qDebug() << "Library: track" << trackId
<< "not in current view, will retry in 1s";
QTimer::singleShot(1000, this, [this, trackId, selectAndReselect]() {
if (m_pLibraryWidget) {
WTrackTableView* pView = m_pLibraryWidget->getCurrentTrackTableView();
if (pView) {
selectAndReselect(pView, trackId);
}
}
});
}
}
}
2 changes: 2 additions & 0 deletions src/library/library.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ class Library: public QObject {
void onSkinLoadFinished();
void slotSaveCurrentViewState() const;
void slotRestoreCurrentViewState() const;
void slotSaveSelectedTrackId();
void slotRestoreSelectedTrackId();

signals:
void showTrackModel(QAbstractItemModel* model, bool restoreState = true);
Expand Down
133 changes: 132 additions & 1 deletion src/library/sidebarmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "library/libraryfeature.h"
#include "library/treeitem.h"
#include "library/treeitemmodel.h"
#include "moc_sidebarmodel.cpp"
#include "util/assert.h"
#include "util/cmdlineargs.h"
Expand All @@ -17,21 +18,39 @@ namespace {
/// been chosen as a compromise between usability and responsiveness.
constexpr int kPressedUntilClickedTimeoutMillis = 300;

/// Debounce delay for saving sidebar selection to config. Avoids
/// excessive config writes while scrolling through items.
constexpr int kSaveDebounceMillis = 3000;

/// Enables additional debugging output.
constexpr bool kDebug = false;

const ConfigKey kLastSelectedFeatureConfigKey =
ConfigKey(QStringLiteral("[Library]"), QStringLiteral("last_selected_feature"));
const ConfigKey kLastSelectedChildConfigKey =
ConfigKey(QStringLiteral("[Library]"), QStringLiteral("last_selected_child"));

} // anonymous namespace

SidebarModel::SidebarModel(
UserSettingsPointer pConfig,
QObject* parent)
: QAbstractItemModel(parent),
m_iDefaultSelectedIndex(0),
m_pressedUntilClickedTimer(new QTimer(this)) {
m_pressedUntilClickedTimer(new QTimer(this)),
m_pConfig(pConfig),
m_saveTimer(new QTimer(this)) {
m_pressedUntilClickedTimer->setSingleShot(true);
connect(m_pressedUntilClickedTimer,
&QTimer::timeout,
this,
&SidebarModel::slotPressedUntilClickedTimeout);

m_saveTimer->setSingleShot(true);
connect(m_saveTimer,
&QTimer::timeout,
this,
&SidebarModel::performSave);
}

void SidebarModel::addLibraryFeature(LibraryFeature* pFeature) {
Expand Down Expand Up @@ -377,6 +396,7 @@ void SidebarModel::clicked(const QModelIndex& index) {
pFeature->activateChild(index);
}
}
scheduleSelectionSave(index);
}
}

Expand Down Expand Up @@ -637,3 +657,114 @@ void SidebarModel::slotFeatureSelect(LibraryFeature* pFeature,
}
emit selectIndex(ind, scrollTo);
}

void SidebarModel::scheduleSelectionSave(const QModelIndex& index) {
if (!index.isValid()) {
return;
}
m_pendingSelection = index;
m_saveTimer->stop();
m_saveTimer->start(kSaveDebounceMillis);
}

void SidebarModel::performSave() {
if (m_pendingSelection.isValid()) {
saveSelectionToConfig(m_pendingSelection);
emit selectionSaved();
}
}

void SidebarModel::saveSelectionToConfig(const QModelIndex& index) {
if (!index.isValid() || !m_pConfig) {
return;
}

LibraryFeature* pFeature = nullptr;

if (index.internalPointer() == this) {
// Top-level feature row
pFeature = m_sFeatures[index.row()];
} else {
TreeItem* pTreeItem = static_cast<TreeItem*>(index.internalPointer());
VERIFY_OR_DEBUG_ASSERT(pTreeItem) {
return;
}
pFeature = pTreeItem->feature();
}

VERIFY_OR_DEBUG_ASSERT(pFeature) {
return;
}

// Save feature icon name for robust matching across sessions
m_pConfig->setValue(kLastSelectedFeatureConfigKey, pFeature->iconName());

// Save child data if it's a child item, clear otherwise
if (index.parent().isValid()) {
QVariant childData = index.data(DataRole);
if (childData.isValid()) {
m_pConfig->set(kLastSelectedChildConfigKey,
ConfigValue(childData.toString()));
} else {
m_pConfig->set(kLastSelectedChildConfigKey, ConfigValue());
}
} else {
m_pConfig->set(kLastSelectedChildConfigKey, ConfigValue());
}
}

bool SidebarModel::restoreLastSelection() {
if (!m_pConfig) {
return false;
}

QString savedFeatureIcon = m_pConfig->getValue(kLastSelectedFeatureConfigKey);
if (savedFeatureIcon.isEmpty()) {
return false;
}

// Find the feature by icon name
LibraryFeature* pTargetFeature = nullptr;
for (int i = 0; i < m_sFeatures.size(); ++i) {
if (m_sFeatures[i]->iconName() == savedFeatureIcon) {
pTargetFeature = m_sFeatures[i];
break;
}
}

if (!pTargetFeature) {
return false;
}

QString savedChildDataStr = m_pConfig->getValue(kLastSelectedChildConfigKey);
if (!savedChildDataStr.isEmpty() && pTargetFeature->sidebarModel()) {
// Try to convert to int first (for playlist/crate IDs), fallback to string
QVariant savedChildData;
bool ok;
int intValue = savedChildDataStr.toInt(&ok);
if (ok) {
savedChildData = intValue;
} else {
savedChildData = savedChildDataStr;
}

TreeItemModel* pChildModel = pTargetFeature->sidebarModel();
const QModelIndexList matches = pChildModel->match(
pChildModel->index(0, 0),
TreeItemModel::kDataRole,
savedChildData,
1,
Qt::MatchExactly | Qt::MatchRecursive);

if (!matches.isEmpty() && matches.first().isValid()) {
// selectAndActivate handles sidebar selection, tree expansion,
// scrolling, and library pane activation
pTargetFeature->selectAndActivate(matches.first());
return true;
}
}

// No child data or child not found — select and activate the feature root
pTargetFeature->selectAndActivate();
return true;
}
14 changes: 14 additions & 0 deletions src/library/sidebarmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
#include <QAbstractItemModel>
#include <QList>
#include <QModelIndex>
#include <QPersistentModelIndex>
#include <QVariant>

#include "preferences/usersettings.h"

class LibraryFeature;
class QTimer;

Expand All @@ -22,13 +25,15 @@ class SidebarModel : public QAbstractItemModel {
Q_ENUM(Roles);

explicit SidebarModel(
UserSettingsPointer pConfig = UserSettingsPointer(),
QObject* parent = nullptr);
~SidebarModel() override = default;

void addLibraryFeature(LibraryFeature* feature);
QModelIndex getDefaultSelection();
void setDefaultSelection(unsigned int index);
void activateDefaultSelection();
bool restoreLastSelection();

// Required for QAbstractItemModel
QModelIndex index(int row, int column,
Expand Down Expand Up @@ -83,6 +88,7 @@ class SidebarModel : public QAbstractItemModel {

signals:
void selectIndex(const QModelIndex& index, bool scrollTo);
void selectionSaved();

private slots:
void slotPressedUntilClickedTimeout();
Expand All @@ -99,6 +105,14 @@ class SidebarModel : public QAbstractItemModel {
QTimer* const m_pressedUntilClickedTimer;
QModelIndex m_pressedIndex;

UserSettingsPointer m_pConfig;
QTimer* const m_saveTimer;
QPersistentModelIndex m_pendingSelection;

void scheduleSelectionSave(const QModelIndex& index);
void performSave();
void saveSelectionToConfig(const QModelIndex& index);

void startPressedUntilClickedTimer(const QModelIndex& pressedIndex);
void stopPressedUntilClickedTimer();
};
3 changes: 2 additions & 1 deletion src/qml/qmlsidebarmodelproxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include "library/treeitem.h"
#include "moc_qmlsidebarmodelproxy.cpp"
#include "qml/qmlconfigproxy.h"
#include "qml/qmllibrarysource.h"
#include "util/assert.h"
#include "util/parented_ptr.h"
Expand Down Expand Up @@ -57,7 +58,7 @@ void QmlSidebarModelProxy::activate(const QModelIndex& index) {
}

QmlSidebarModelProxy::QmlSidebarModelProxy(QObject* parent)
: SidebarModel(parent),
: SidebarModel(QmlConfigProxy::get(), parent),
m_tracklist(nullptr) {
}
QmlSidebarModelProxy::~QmlSidebarModelProxy() = default;
Expand Down
Loading