Skip to content

Commit ad5067a

Browse files
author
Milkii Brewster
committed
restore library sidebar state on startup
persist and restore both selected item and scroll position across sessions for seamless startup experience. selection restoration: - stores feature iconname (non-translated) and child data - uses QAbstractItemModel::match for robust child lookup - handles playlists, crates, and feature roots - config keys: [Library] LastSelectedFeature, LastSelectedChild scroll restoration: - saves scroll position on every change - restores after tree expansion completes - config key: [Library] SidebarScrollPosition prevents jarring position changes on startup while maintaining natural sidebar behavior during runtime
1 parent 6adb012 commit ad5067a

6 files changed

Lines changed: 238 additions & 35 deletions

File tree

src/library/library.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ Library::Library(
7979
m_pAnalysisFeature(nullptr) {
8080
qRegisterMetaType<LibraryRemovalType>("LibraryRemovalType");
8181

82+
// provide config to sidebar model for selection persistence
83+
m_pSidebarModel->setConfig(m_pConfig);
84+
8285
m_pKeyNotation.reset(
8386
new ControlObject(mixxx::library::prefs::kKeyNotationConfigKey));
8487

@@ -349,6 +352,7 @@ void Library::bindSidebarWidget(WLibrarySidebar* pSidebarWidget) {
349352

350353
// Setup the sources view
351354
pSidebarWidget->setModel(m_pSidebarModel);
355+
pSidebarWidget->setup(m_pConfig);
352356
connect(m_pSidebarModel,
353357
&SidebarModel::selectIndex,
354358
pSidebarWidget,

src/library/sidebarmodel.cpp

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <QTimer>
44
#include <QUrl>
5+
#include <functional>
56

67
#include "library/libraryfeature.h"
78
#include "library/treeitem.h"
@@ -94,9 +95,20 @@ void SidebarModel::setDefaultSelection(unsigned int index) {
9495
}
9596

9697
void SidebarModel::activateDefaultSelection() {
98+
// try to restore previously selected item
99+
QModelIndex savedIndex = restoreSavedSelection();
100+
if (savedIndex.isValid()) {
101+
// disable auto-scrolling - sidebar will restore saved scroll position
102+
emit selectIndex(savedIndex, false /* scrollTo */);
103+
// reuse clicked() method to activate the restored selection
104+
clicked(savedIndex);
105+
return;
106+
}
107+
108+
// fallback to default selection if restoration failed
97109
if (m_iDefaultSelectedIndex <
98110
static_cast<unsigned int>(m_sFeatures.size())) {
99-
emit selectIndex(getDefaultSelection(), true /* scrollTo */);
111+
emit selectIndex(getDefaultSelection(), false /* scrollTo */);
100112
// Selecting an index does not activate it.
101113
m_sFeatures[m_iDefaultSelectedIndex]->activate();
102114
}
@@ -354,6 +366,9 @@ void SidebarModel::clicked(const QModelIndex& index) {
354366
// this event.
355367
stopPressedUntilClickedTimer();
356368
if (index.isValid()) {
369+
// save the selection for restoration on restart
370+
saveCurrentSelection(index);
371+
357372
if (index.internalPointer() == this) {
358373
m_sFeatures[index.row()]->activate();
359374
} else {
@@ -520,7 +535,16 @@ QModelIndex SidebarModel::translateIndex(
520535
void SidebarModel::slotDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) {
521536
// qDebug() << "slotDataChanged topLeft:" << topLeft << "bottomRight:" << bottomRight;
522537
QModelIndex topLeftTranslated = translateSourceIndex(topLeft);
523-
QModelIndex bottomRightTranslated = translateSourceIndex(bottomRight);
538+
QModelIndex bottomRightTranslated;
539+
540+
// if bottomRight is invalid in the source, keep it invalid in the translation
541+
if (bottomRight.isValid()) {
542+
bottomRightTranslated = translateSourceIndex(bottomRight);
543+
} else {
544+
// per Qt convention, invalid bottomRight means same as topLeft
545+
bottomRightTranslated = topLeftTranslated;
546+
}
547+
524548
emit dataChanged(topLeftTranslated, bottomRightTranslated);
525549
}
526550

@@ -610,3 +634,131 @@ void SidebarModel::slotFeatureSelect(LibraryFeature* pFeature,
610634
}
611635
emit selectIndex(ind, scrollTo);
612636
}
637+
638+
void SidebarModel::saveCurrentSelection(const QModelIndex& index) {
639+
if (!m_pConfig || !index.isValid()) {
640+
return;
641+
}
642+
643+
QString featureName;
644+
QString childName;
645+
646+
if (index.internalPointer() == this) {
647+
// root feature selected
648+
if (index.row() >= 0 && index.row() < m_sFeatures.size()) {
649+
featureName = m_sFeatures[index.row()]->iconName();
650+
}
651+
} else {
652+
// child item selected
653+
TreeItem* pTreeItem = static_cast<TreeItem*>(index.internalPointer());
654+
if (pTreeItem) {
655+
LibraryFeature* pFeature = pTreeItem->feature();
656+
if (pFeature) {
657+
featureName = pFeature->iconName();
658+
childName = pTreeItem->getData().toString();
659+
}
660+
}
661+
}
662+
663+
if (!featureName.isEmpty()) {
664+
m_pConfig->setValue(
665+
ConfigKey(QStringLiteral("[Library]"), QStringLiteral("LastSelectedFeature")),
666+
featureName);
667+
m_pConfig->setValue(
668+
ConfigKey(QStringLiteral("[Library]"), QStringLiteral("LastSelectedChild")),
669+
childName);
670+
}
671+
}
672+
673+
QModelIndex SidebarModel::restoreSavedSelection() {
674+
if (!m_pConfig || m_sFeatures.isEmpty()) {
675+
return QModelIndex();
676+
}
677+
678+
const QString savedFeatureName = m_pConfig->getValueString(
679+
ConfigKey(QStringLiteral("[Library]"), QStringLiteral("LastSelectedFeature")));
680+
681+
if (savedFeatureName.isEmpty()) {
682+
return QModelIndex();
683+
}
684+
685+
int featureRow = -1;
686+
// find the feature by icon name (non-translated, programmatic identifier)
687+
for (int i = 0; i < m_sFeatures.size(); ++i) {
688+
if (m_sFeatures[i]->iconName() == savedFeatureName) {
689+
featureRow = i;
690+
break;
691+
}
692+
}
693+
694+
const QString savedChildName = m_pConfig->getValueString(
695+
ConfigKey(QStringLiteral("[Library]"), QStringLiteral("LastSelectedChild")));
696+
// if no child data, return the feature root.
697+
// returns invalid index if feature was not found.
698+
if (savedChildName.isEmpty()) {
699+
return index(featureRow, 0);
700+
}
701+
702+
// feature not found, can't search for child
703+
if (featureRow < 0) {
704+
return {};
705+
}
706+
707+
// search for matching child by data using QAbstractItemModel::match()
708+
QAbstractItemModel* pChildModel = m_sFeatures[featureRow]->sidebarModel();
709+
if (pChildModel && pChildModel->rowCount() > 0) {
710+
// the model items' data type may be int (playlist, crate) or
711+
// QString (Missing, Quick Links).
712+
// get the model's data type, then create the appropriate QVariant.
713+
// note: assumes the model uses only one data type!
714+
const QVariant dataVar = pChildModel->data(
715+
pChildModel->index(0, 0), TreeItemModel::kDataRole);
716+
auto dataType = dataVar.metaType();
717+
QVariant childData;
718+
switch (dataType.id()) {
719+
case QMetaType::QString: {
720+
childData = QVariant::fromValue(savedChildName);
721+
break;
722+
}
723+
case QMetaType::Int: {
724+
bool convertedToInt = false;
725+
int intValue = savedChildName.toInt(&convertedToInt);
726+
if (convertedToInt) {
727+
childData = QVariant::fromValue(intValue);
728+
} else {
729+
qWarning() << "SidebarModel::restoreSavedSelection: could not "
730+
"convert stored child data to int";
731+
}
732+
break;
733+
}
734+
default:
735+
qWarning() << "SidebarModel::restoreSavedSelection: "
736+
"model uses unexpected data type"
737+
<< dataType.name();
738+
qWarning() << "select feature root";
739+
}
740+
741+
if (!childData.isValid()) {
742+
return {};
743+
}
744+
745+
const QModelIndexList matches = pChildModel->match(
746+
pChildModel->index(0, 0),
747+
TreeItemModel::kDataRole,
748+
childData,
749+
1, // stop at first match
750+
Qt::MatchExactly | Qt::MatchRecursive);
751+
752+
if (!matches.isEmpty() && matches.first().isValid()) {
753+
// translate child model index to sidebar model index
754+
const QModelIndex childIndex = matches.first();
755+
TreeItem* pTreeItem = static_cast<TreeItem*>(childIndex.internalPointer());
756+
if (pTreeItem) {
757+
return createIndex(childIndex.row(), childIndex.column(), pTreeItem);
758+
}
759+
}
760+
}
761+
762+
// child not found, return feature root or invalid index if feature was not found.
763+
return index(featureRow, 0);
764+
}

src/library/sidebarmodel.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
#include <QAbstractItemModel>
44
#include <QList>
55
#include <QModelIndex>
6+
#include <QString>
67
#include <QVariant>
78

9+
#include "preferences/usersettings.h"
10+
811
class LibraryFeature;
912
class QTimer;
1013

@@ -25,6 +28,10 @@ class SidebarModel : public QAbstractItemModel {
2528
QObject* parent = nullptr);
2629
~SidebarModel() override = default;
2730

31+
void setConfig(UserSettingsPointer pConfig) {
32+
m_pConfig = pConfig;
33+
}
34+
2835
void addLibraryFeature(LibraryFeature* feature);
2936
QModelIndex getDefaultSelection();
3037
void setDefaultSelection(unsigned int index);
@@ -93,10 +100,14 @@ class SidebarModel : public QAbstractItemModel {
93100
QModelIndex translateSourceIndex(const QModelIndex& parent);
94101
QModelIndex translateIndex(const QModelIndex& index, const QAbstractItemModel* model);
95102
void featureRenamed(LibraryFeature*);
103+
void saveCurrentSelection(const QModelIndex& index);
104+
QModelIndex restoreSavedSelection();
105+
96106
unsigned int m_iDefaultSelectedIndex; /** Index of the item in the sidebar model to select at startup. */
97107

98108
QTimer* const m_pressedUntilClickedTimer;
99109
QModelIndex m_pressedIndex;
110+
UserSettingsPointer m_pConfig;
100111

101112
void startPressedUntilClickedTimer(const QModelIndex& pressedIndex);
102113
void stopPressedUntilClickedTimer();

src/library/treeitemmodel.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,11 @@ void TreeItemModel::triggerRepaint(const QModelIndex& index) {
214214
}
215215

216216
void TreeItemModel::triggerRepaint() {
217+
// don't emit dataChanged if the model is empty
218+
if (rowCount() == 0) {
219+
return;
220+
}
221+
217222
QModelIndex left = index(0, 0);
218223
QModelIndex right = index(rowCount() - 1, columnCount() - 1);
219224
emit dataChanged(left, right);

0 commit comments

Comments
 (0)