Skip to content

Commit f188d9a

Browse files
author
Milkii Brewster
committed
restore last selected library feature/playlist/crate on restart
implements #10125 persists the currently selected sidebar item (feature or child) to user settings and restores it on next mixxx startup. uses feature iconname + child data for robust matching across sessions. gracefully falls back to default selection if saved item not found. config keys: - [Library] LastSelectedFeature: stores feature icon name (programmatic identifier) - [Library] LastSelectedChild: stores child item data (playlist/crate id) changes based on pr feedback: - use iconName() instead of title() for feature identification (non-translated, version-stable) - use QAbstractItemModel::match() with recursive flag to simplify child lookup - add bounds checking to prevent out-of-bounds access - reuse clicked() method for activation to eliminate code duplication also adds Qt6::GuiPrivate component for Qt >= 6.10 compatibility prevent jarring sidebar scroll on startup - only scroll if restored item not visible in viewport - position at top when scrolling needed - disable auto-scroll during startup restoration
1 parent 6adb012 commit f188d9a

9 files changed

Lines changed: 226 additions & 32 deletions

File tree

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3518,6 +3518,10 @@ if(QT6)
35183518
list(APPEND QT_EXTRA_COMPONENTS "ShaderTools")
35193519
list(APPEND QT_EXTRA_COMPONENTS "SvgWidgets")
35203520
list(APPEND QT_EXTRA_COMPONENTS "Core5Compat")
3521+
if(QT_VERSION VERSION_GREATER_EQUAL 6.10)
3522+
# from Qt 6.10 GuiPrivate required for QShader/rendergraph (rhi/qshader.h)
3523+
list(APPEND QT_EXTRA_COMPONENTS "GuiPrivate")
3524+
endif()
35213525
else()
35223526
find_package(QT 5.12 NAMES Qt5 COMPONENTS Core REQUIRED)
35233527
endif()

res/qml/MicrophoneDuckingPanel.qml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Column {
4040
case MicrophoneDuckingPanel.DuckingMode.Auto:
4141
return "Auto";
4242
case MicrophoneDuckingPanel.DuckingMode.Manual:
43-
return "Manual";
43+
return "Manual";
4444
default:
4545
return "Off";
4646
}

res/qml/Mixxx/Controls/Knob.qml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Item {
2727
case Knob.ArcStart.Minimum:
2828
return min;
2929
case Knob.ArcStart.Maximum:
30-
return max;
30+
return max;
3131
default:
3232
return valueCenter;
3333
}

res/qml/SyncButton.qml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@ Skin.Button {
2828
case SyncButton.SyncMode.ImplicitLeader:
2929
return Theme.yellow;
3030
case SyncButton.SyncMode.ExplicitLeader:
31-
return Theme.red;
31+
return Theme.red;
3232
default:
3333
return Theme.deckActiveColor;
3434
}
3535
}
3636
text: {
3737
switch (mode) {
3838
case SyncButton.SyncMode.ImplicitLeader:
39-
case SyncButton.SyncMode.ExplicitLeader:
40-
return "Leader";
39+
case SyncButton.SyncMode.ExplicitLeader:
40+
return "Leader";
4141
default:
4242
return "Sync";
4343
}

src/library/library.cpp

Lines changed: 3 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

src/library/sidebarmodel.cpp

Lines changed: 153 additions & 1 deletion
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,6 +95,17 @@ 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+
// enable smart scrolling - only if item below viewport
102+
emit selectIndex(savedIndex, true /* 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())) {
99111
emit selectIndex(getDefaultSelection(), true /* scrollTo */);
@@ -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)