From fbb0871b6feeb031c7df90f605a9ed509078d00d Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Thu, 30 Dec 2021 00:54:54 +0100 Subject: [PATCH 1/6] feat(qml): Add TableFromListModel QML type to map list roles to table --- CMakeLists.txt | 2 + src/qml/qmlapplication.cpp | 2 + src/qml/qmltablefromlistmodel.cpp | 268 ++++++++++++++++++++++++ src/qml/qmltablefromlistmodel.h | 98 +++++++++ src/qml/qmltablefromlistmodelcolumn.cpp | 211 +++++++++++++++++++ src/qml/qmltablefromlistmodelcolumn.h | 164 +++++++++++++++ 6 files changed, 745 insertions(+) create mode 100644 src/qml/qmltablefromlistmodel.cpp create mode 100644 src/qml/qmltablefromlistmodel.h create mode 100644 src/qml/qmltablefromlistmodelcolumn.cpp create mode 100644 src/qml/qmltablefromlistmodelcolumn.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ca6a4892936..01b6eed4ed97 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1092,6 +1092,8 @@ if(QT6) src/qml/qmllibrarytracklistmodel.cpp src/qml/qmlplayermanagerproxy.cpp src/qml/qmlplayerproxy.cpp + src/qml/qmltablefromlistmodel.cpp + src/qml/qmltablefromlistmodelcolumn.cpp src/qml/qmlvisibleeffectsmodel.cpp src/qml/qmlwaveformoverview.cpp ) diff --git a/src/qml/qmlapplication.cpp b/src/qml/qmlapplication.cpp index d2862b709b0b..e3b9976b7ffb 100644 --- a/src/qml/qmlapplication.cpp +++ b/src/qml/qmlapplication.cpp @@ -16,6 +16,8 @@ #include "qml/qmllibrarytracklistmodel.h" #include "qml/qmlplayermanagerproxy.h" #include "qml/qmlplayerproxy.h" +#include "qml/qmltablefromlistmodel.h" +#include "qml/qmltablefromlistmodelcolumn.h" #include "qml/qmlvisibleeffectsmodel.h" #include "qml/qmlwaveformoverview.h" #include "soundio/soundmanager.h" diff --git a/src/qml/qmltablefromlistmodel.cpp b/src/qml/qmltablefromlistmodel.cpp new file mode 100644 index 000000000000..e6fdf4c40866 --- /dev/null +++ b/src/qml/qmltablefromlistmodel.cpp @@ -0,0 +1,268 @@ +#include "qml/qmltablefromlistmodel.h" + +#include +#include +#include + +#include "util/assert.h" + +namespace mixxx { +namespace qml { +QmlTableFromListModel::QmlTableFromListModel(QObject* parent) + : QAbstractTableModel(parent), + m_columnCount(0), + m_pSourceModel(nullptr) { +} + +QmlTableFromListModel::~QmlTableFromListModel() { +} + +QAbstractItemModel* QmlTableFromListModel::sourceModel() const { + return m_pSourceModel; +} +void QmlTableFromListModel::setSourceModel(QAbstractItemModel* pSourceModel) { + if (m_pSourceModel == pSourceModel) { + return; + } + + beginResetModel(); + if (m_pSourceModel != nullptr) { + disconnect(m_pSourceModel, nullptr, this, nullptr); + } + + m_sourceRoles.clear(); + m_pSourceModel = pSourceModel; + if (m_pSourceModel != nullptr) { + const QHash& sourceRoleNames = m_pSourceModel->roleNames(); + for (auto it = sourceRoleNames.cbegin(); it != sourceRoleNames.cend(); it++) { + int roleId = it.key(); + m_sourceRoles.insert(QString::fromUtf8(it.value()), roleId); + } + connect(pSourceModel, + &QAbstractListModel::dataChanged, + this, + [this](const QModelIndex& sourceTopLeft, + const QModelIndex& sourceBottomRight, + const QVector& roles) { + Q_UNUSED(roles); + + const QModelIndex topLeft = index(sourceTopLeft.row(), 0); + const QModelIndex bottomRight = + index(sourceBottomRight.row(), columnCount()); + emit dataChanged(topLeft, bottomRight); + }); + } + emit sourceModelChanged(); + endResetModel(); + + if (componentCompleted) { + fetchColumnMetadata(); + } +} + +void QmlTableFromListModel::classBegin() { +} +void QmlTableFromListModel::componentComplete() { + componentCompleted = true; + + m_columnCount = m_columns.size(); + if (m_columnCount > 0) + emit columnCountChanged(); + + fetchColumnMetadata(); +} +QQmlListProperty QmlTableFromListModel::columns() { + return QQmlListProperty(this, + nullptr, + &QmlTableFromListModel::columns_append, + &QmlTableFromListModel::columns_count, + &QmlTableFromListModel::columns_at, + &QmlTableFromListModel::columns_clear); +} +void QmlTableFromListModel::columns_append( + QQmlListProperty* pProperty, + QmlTableFromListModelColumn* value) { + QmlTableFromListModel* pModel = static_cast(pProperty->object); + QmlTableFromListModelColumn* pColumn = qobject_cast(value); + if (pColumn) { + pModel->m_columns.append(pColumn); + } +} +qsizetype QmlTableFromListModel::columns_count( + QQmlListProperty* pProperty) { + const QmlTableFromListModel* pModel = static_cast(pProperty->object); + return pModel->m_columns.count(); +} +QmlTableFromListModelColumn* QmlTableFromListModel::columns_at( + QQmlListProperty* pProperty, qsizetype index) { + const QmlTableFromListModel* pModel = static_cast(pProperty->object); + return pModel->m_columns.at(index); +} +void QmlTableFromListModel::columns_clear( + QQmlListProperty* pProperty) { + QmlTableFromListModel* pModel = static_cast(pProperty->object); + return pModel->m_columns.clear(); +} +QModelIndex QmlTableFromListModel::index(int row, int column, const QModelIndex& parent) const { + return row >= 0 && row < rowCount() && column >= 0 && + column < columnCount() && !parent.isValid() + ? createIndex(row, column) + : QModelIndex(); +} +int QmlTableFromListModel::rowCount(const QModelIndex& parent) const { + if (parent.isValid() || m_pSourceModel == nullptr) { + return 0; + } + return m_pSourceModel->rowCount(); +} +int QmlTableFromListModel::columnCount(const QModelIndex& parent) const { + if (parent.isValid()) { + return 0; + } + + return m_columnCount; +} +QVariant QmlTableFromListModel::data(const QModelIndex& index, const QString& role) const { + const int roleId = roleNames().key(role.toUtf8(), -1); + if (roleId >= 0) { + return data(index, roleId); + } + return QVariant(); +} +QVariant QmlTableFromListModel::data(const QModelIndex& index, int role) const { + if (m_pSourceModel == nullptr) { + return QVariant(); + } + + const int row = index.row(); + if (row < 0 || row >= rowCount()) { + return QVariant(); + } + const int column = index.column(); + if (column < 0 || column >= columnCount()) { + return QVariant(); + } + const ColumnMetadata columnMetadata = m_columnMetadata.at(index.column()); + const QString roleName = QString::fromUtf8(roleNames().value(role)); + if (!columnMetadata.roles.contains(roleName)) { + qWarning() << "data(): no role named " << roleName + << " at column index " << column << ". The available roles for that column are: " + << columnMetadata.roles.keys(); + return QVariant(); + } + const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName); + const int sourceRole = m_sourceRoles.value(roleData.name); + const QModelIndex modelIndex = m_pSourceModel->index(row, 0); + return m_pSourceModel->data(modelIndex, sourceRole); +} +bool QmlTableFromListModel::setData( + const QModelIndex& index, const QString& role, const QVariant& value) { + const int intRole = roleNames().key(role.toUtf8(), -1); + if (intRole >= 0) { + return setData(index, value, intRole); + } + return false; +} +bool QmlTableFromListModel::setData(const QModelIndex& index, const QVariant& value, int role) { + if (m_pSourceModel == nullptr) { + return false; + } + + const int row = index.row(); + if (row < 0 || row >= rowCount()) + return false; + const int column = index.column(); + if (column < 0 || column >= columnCount()) + return false; + const QString roleName = QString::fromUtf8(roleNames().value(role)); + qDebug().nospace() << "setData() called with index " + << index << ", value " << value << " and role " << roleName; + // Verify that the role exists for this column. + const ColumnMetadata columnMetadata = m_columnMetadata.at(index.column()); + if (!columnMetadata.roles.contains(roleName)) { + qWarning() << "setData(): no role named \"" << roleName + << "\" at column index " << column + << ". The available roles for that column are: " + << columnMetadata.roles.keys(); + return false; + } + // Verify that the type of the value is what we expect. + // If the value set is not of the expected type, we can try to convert it automatically. + const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName); + const int sourceRole = m_sourceRoles.value(roleData.name); + const QModelIndex modelIndex = m_pSourceModel->index(row, 0); + return m_pSourceModel->setData(modelIndex, value, sourceRole); +} + +QmlTableFromListModel::ColumnRoleMetadata +QmlTableFromListModel::fetchColumnRoleData(const QString& roleNameKey, + QmlTableFromListModelColumn* pColumn, + int columnIndex) const { + DEBUG_ASSERT(componentCompleted); + + ColumnRoleMetadata roleData; + VERIFY_OR_DEBUG_ASSERT(m_pSourceModel != nullptr) { + return roleData; + } + + QJSValue columnRoleGetter = pColumn->getterAtRole(roleNameKey); + if (columnRoleGetter.isUndefined()) { + // This role is not defined, which is fine; just skip it. + return roleData; + } + if (columnRoleGetter.isString()) { + const QString rolePropertyName = columnRoleGetter.toString(); + roleData.name = rolePropertyName; + roleData.roleId = m_sourceRoles.value(rolePropertyName); + } else { + // Invalid role. + qWarning() << "TableFromListModelColumn role for column at index " + << columnIndex << " must be either a string or a function; actual type is: " + << columnRoleGetter.toString(); + } + return roleData; +} + +void QmlTableFromListModel::fetchColumnMetadata() { + qDebug() << "gathering metadata for" << m_columnCount << "columns"; + m_columnMetadata.clear(); + static const auto supportedRoleNames = QmlTableFromListModelColumn::supportedRoleNames(); + // Since we support different data structures at the row level, we require that there + // is a TableFromListModelColumn for each column. + // Collect and cache metadata for each column. This makes data lookup faster. + for (int columnIndex = 0; columnIndex < m_columns.size(); ++columnIndex) { + QmlTableFromListModelColumn* column = m_columns.at(columnIndex); + qDebug().nospace() << "- column " << columnIndex << ":"; + ColumnMetadata metaData; + const auto builtInRoleKeys = supportedRoleNames.keys(); + for (const int builtInRoleKey : builtInRoleKeys) { + const QString builtInRoleName = supportedRoleNames.value(builtInRoleKey); + ColumnRoleMetadata roleData = fetchColumnRoleData(builtInRoleName, column, columnIndex); + if (!roleData.isValid()) { + // This built-in role was not specified in this column. + continue; + } + qDebug().nospace() << " - added metadata for built-in role " + << builtInRoleName << " at column index " << columnIndex << ": name=" + << roleData.name << "roleId=" << roleData.roleId; + // This column now supports this specific built-in role. + metaData.roles.insert(builtInRoleName, roleData); + } + m_columnMetadata.insert(columnIndex, metaData); + } +} + +QmlTableFromListModel::ColumnRoleMetadata::ColumnRoleMetadata() { +} + +QmlTableFromListModel::ColumnRoleMetadata::ColumnRoleMetadata(const QString& name, int roleId) + : name(name), + roleId(roleId) { +} + +bool QmlTableFromListModel::ColumnRoleMetadata::isValid() const { + return !name.isEmpty() && roleId >= 0; +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmltablefromlistmodel.h b/src/qml/qmltablefromlistmodel.h new file mode 100644 index 000000000000..b7a7c72d3440 --- /dev/null +++ b/src/qml/qmltablefromlistmodel.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "qml/qmltablefromlistmodelcolumn.h" + +namespace mixxx { +namespace qml { + +class QmlTableFromListModel : public QAbstractTableModel, public QQmlParserStatus { + Q_OBJECT + Q_PROPERTY(int columnCount READ columnCount NOTIFY columnCountChanged FINAL) + Q_PROPERTY(QAbstractItemModel* sourceModel READ sourceModel WRITE + setSourceModel NOTIFY sourceModelChanged REQUIRED FINAL) + Q_PROPERTY(QQmlListProperty columns + READ columns CONSTANT FINAL) + Q_INTERFACES(QQmlParserStatus) + Q_CLASSINFO("DefaultProperty", "columns") + QML_NAMED_ELEMENT(TableFromListModel) + public: + QmlTableFromListModel(QObject* parent = nullptr); + ~QmlTableFromListModel() override; + + QAbstractItemModel* sourceModel() const; + void setSourceModel(QAbstractItemModel* pSourceModel); + + QQmlListProperty columns(); + static void columns_append( + QQmlListProperty* property, + QmlTableFromListModelColumn* value); + static qsizetype columns_count(QQmlListProperty* property); + static QmlTableFromListModelColumn* columns_at( + QQmlListProperty* property, qsizetype index); + static void columns_clear(QQmlListProperty* property); + + QModelIndex index(int row, + int column, + const QModelIndex& parent = QModelIndex()) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + Q_INVOKABLE QVariant data(const QModelIndex& index, const QString& role) const; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + Q_INVOKABLE bool setData(const QModelIndex& index, const QString& role, const QVariant& value); + bool setData(const QModelIndex& index, + const QVariant& value, + int role = Qt::DisplayRole) override; + + signals: + void columnCountChanged(); + void sourceModelChanged(); + + private: + class ColumnRoleMetadata { + public: + ColumnRoleMetadata(); + ColumnRoleMetadata(const QString& name, int roleId); + bool isValid() const; + // If this is false, it's a function role. + QString name; + int roleId = -1; + }; + struct ColumnMetadata { + // Key = role name that will be made visible to the delegate + // Value = metadata about that role, including actual name in the model data, type, etc. + QHash roles; + }; + enum NewRowOperationFlag { + OtherOperation, // insert(), set(), etc. + SetRowsOperation, + AppendOperation + }; + + void classBegin() override; + void componentComplete() override; + bool componentCompleted = false; + + QmlTableFromListModel::ColumnRoleMetadata fetchColumnRoleData( + const QString& roleNameKey, + QmlTableFromListModelColumn* pColumn, + int columnIndex) const; + void fetchColumnMetadata(); + + QList m_columns; + int m_columnCount = 0; + // Each entry contains information about the properties of the column at that index. + QVector m_columnMetadata; + // key = property index (0 to number of properties across all columns) + // value = role name + QAbstractItemModel* m_pSourceModel; + QHash m_sourceRoles; +}; + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmltablefromlistmodelcolumn.cpp b/src/qml/qmltablefromlistmodelcolumn.cpp new file mode 100644 index 000000000000..c2c61e2a4b30 --- /dev/null +++ b/src/qml/qmltablefromlistmodelcolumn.cpp @@ -0,0 +1,211 @@ +#include "qml/qmltablefromlistmodelcolumn.h" + +#include +#include +#include + +namespace { + +const QString displayRoleName = QStringLiteral("display"); +const QString decorationRoleName = QStringLiteral("decoration"); +const QString editRoleName = QStringLiteral("edit"); +const QString toolTipRoleName = QStringLiteral("toolTip"); +const QString statusTipRoleName = QStringLiteral("statusTip"); +const QString whatsThisRoleName = QStringLiteral("whatsThis"); +const QString fontRoleName = QStringLiteral("font"); +const QString textAlignmentRoleName = QStringLiteral("textAlignment"); +const QString backgroundRoleName = QStringLiteral("background"); +const QString foregroundRoleName = QStringLiteral("foreground"); +const QString checkStateRoleName = QStringLiteral("checkState"); +const QString accessibleTextRoleName = QStringLiteral("accessibleText"); +const QString accessibleDescriptionRoleName = QStringLiteral("accessibleDescription"); +const QString sizeHintRoleName = QStringLiteral("sizeHint"); + +} // namespace + +namespace mixxx { +namespace qml { +QmlTableFromListModelColumn::QmlTableFromListModelColumn(QObject* parent) + : QObject(parent) { +} +QmlTableFromListModelColumn::~QmlTableFromListModelColumn() { +} +#define DEFINE_ROLE_PROPERTIES(getterGetterName, \ + getterSetterName, \ + getterSignal, \ + setterGetterName, \ + setterSetterName, \ + setterSignal, \ + roleName) \ + QJSValue QmlTableFromListModelColumn::getterGetterName() const { \ + return m_getters.value(roleName); \ + } \ + \ + void QmlTableFromListModelColumn::getterSetterName( \ + const QJSValue& stringOrFunction) { \ + if (!stringOrFunction.isString() && !stringOrFunction.isCallable()) { \ + qWarning().quote() \ + << "getter for " << roleName << " must be a function"; \ + return; \ + } \ + \ + if (stringOrFunction.strictlyEquals(getterGetterName())) { \ + return; \ + } \ + \ + m_getters[roleName] = stringOrFunction; \ + emit getterSignal(); \ + } \ + \ + QJSValue QmlTableFromListModelColumn::setterGetterName() const { \ + return m_setters.value(roleName); \ + } \ + \ + void QmlTableFromListModelColumn::setterSetterName( \ + const QJSValue& function) { \ + if (!function.isCallable()) { \ + qWarning().quote() \ + << "setter for " << roleName << " must be a function"; \ + return; \ + } \ + \ + if (function.strictlyEquals(setterGetterName())) { \ + return; \ + } \ + \ + m_setters[roleName] = function; \ + emit setterSignal(); \ + } +DEFINE_ROLE_PROPERTIES(display, + setDisplay, + displayChanged, + getSetDisplay, + setSetDisplay, + setDisplayChanged, + displayRoleName) +DEFINE_ROLE_PROPERTIES(decoration, + setDecoration, + decorationChanged, + getSetDecoration, + setSetDecoration, + setDecorationChanged, + decorationRoleName) +DEFINE_ROLE_PROPERTIES(edit, + setEdit, + editChanged, + getSetEdit, + setSetEdit, + setEditChanged, + editRoleName) +DEFINE_ROLE_PROPERTIES(toolTip, + setToolTip, + toolTipChanged, + getSetToolTip, + setSetToolTip, + setToolTipChanged, + toolTipRoleName) +DEFINE_ROLE_PROPERTIES(statusTip, + setStatusTip, + statusTipChanged, + getSetStatusTip, + setSetStatusTip, + setStatusTipChanged, + statusTipRoleName) +DEFINE_ROLE_PROPERTIES(whatsThis, + setWhatsThis, + whatsThisChanged, + getSetWhatsThis, + setSetWhatsThis, + setWhatsThisChanged, + whatsThisRoleName) +DEFINE_ROLE_PROPERTIES(font, + setFont, + fontChanged, + getSetFont, + setSetFont, + setFontChanged, + fontRoleName) +DEFINE_ROLE_PROPERTIES(textAlignment, + setTextAlignment, + textAlignmentChanged, + getSetTextAlignment, + setSetTextAlignment, + setTextAlignmentChanged, + textAlignmentRoleName) +DEFINE_ROLE_PROPERTIES(background, + setBackground, + backgroundChanged, + getSetBackground, + setSetBackground, + setBackgroundChanged, + backgroundRoleName) +DEFINE_ROLE_PROPERTIES(foreground, + setForeground, + foregroundChanged, + getSetForeground, + setSetForeground, + setForegroundChanged, + foregroundRoleName) +DEFINE_ROLE_PROPERTIES(checkState, + setCheckState, + checkStateChanged, + getSetCheckState, + setSetCheckState, + setCheckStateChanged, + checkStateRoleName) +DEFINE_ROLE_PROPERTIES(accessibleText, + setAccessibleText, + accessibleTextChanged, + getSetAccessibleText, + setSetAccessibleText, + setAccessibleTextChanged, + accessibleTextRoleName) +DEFINE_ROLE_PROPERTIES(accessibleDescription, + setAccessibleDescription, + accessibleDescriptionChanged, + getSetAccessibleDescription, + setSetAccessibleDescription, + setAccessibleDescriptionChanged, + accessibleDescriptionRoleName) +DEFINE_ROLE_PROPERTIES(sizeHint, + setSizeHint, + sizeHintChanged, + getSetSizeHint, + setSetSizeHint, + setSizeHintChanged, + sizeHintRoleName) +QJSValue QmlTableFromListModelColumn::getterAtRole(const QString& roleName) { + auto it = m_getters.find(roleName); + if (it == m_getters.end()) + return QJSValue(); + return *it; +} +QJSValue QmlTableFromListModelColumn::setterAtRole(const QString& roleName) { + auto it = m_setters.find(roleName); + if (it == m_setters.end()) + return QJSValue(); + return *it; +} +const QHash QmlTableFromListModelColumn::getters() const { + return m_getters; +} +const QHash QmlTableFromListModelColumn::supportedRoleNames() { + QHash names; + names[Qt::DisplayRole] = QLatin1String("display"); + names[Qt::DecorationRole] = QLatin1String("decoration"); + names[Qt::EditRole] = QLatin1String("edit"); + names[Qt::ToolTipRole] = QLatin1String("toolTip"); + names[Qt::StatusTipRole] = QLatin1String("statusTip"); + names[Qt::WhatsThisRole] = QLatin1String("whatsThis"); + names[Qt::FontRole] = QLatin1String("font"); + names[Qt::TextAlignmentRole] = QLatin1String("textAlignment"); + names[Qt::BackgroundRole] = QLatin1String("background"); + names[Qt::ForegroundRole] = QLatin1String("foreground"); + names[Qt::CheckStateRole] = QLatin1String("checkState"); + names[Qt::AccessibleTextRole] = QLatin1String("accessibleText"); + names[Qt::AccessibleDescriptionRole] = QLatin1String("accessibleDescription"); + names[Qt::SizeHintRole] = QLatin1String("sizeHint"); + return names; +} +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmltablefromlistmodelcolumn.h b/src/qml/qmltablefromlistmodelcolumn.h new file mode 100644 index 000000000000..9edbe23fa7b5 --- /dev/null +++ b/src/qml/qmltablefromlistmodelcolumn.h @@ -0,0 +1,164 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace mixxx { +namespace qml { + +class QmlTableFromListModelColumn : public QObject { + Q_OBJECT + Q_PROPERTY(QJSValue display READ display WRITE setDisplay NOTIFY displayChanged FINAL) + Q_PROPERTY(QJSValue setDisplay READ getSetDisplay WRITE setSetDisplay NOTIFY setDisplayChanged) + Q_PROPERTY(QJSValue decoration READ decoration WRITE setDecoration NOTIFY + decorationChanged FINAL) + Q_PROPERTY(QJSValue setDecoration READ getSetDecoration WRITE + setSetDecoration NOTIFY setDecorationChanged FINAL) + Q_PROPERTY(QJSValue edit READ edit WRITE setEdit NOTIFY editChanged FINAL) + Q_PROPERTY(QJSValue setEdit READ getSetEdit WRITE setSetEdit NOTIFY setEditChanged FINAL) + Q_PROPERTY(QJSValue toolTip READ toolTip WRITE setToolTip NOTIFY toolTipChanged FINAL) + Q_PROPERTY(QJSValue setToolTip READ getSetToolTip WRITE setSetToolTip NOTIFY + setToolTipChanged FINAL) + Q_PROPERTY(QJSValue statusTip READ statusTip WRITE setStatusTip NOTIFY statusTipChanged FINAL) + Q_PROPERTY(QJSValue setStatusTip READ getSetStatusTip WRITE setSetStatusTip + NOTIFY setStatusTipChanged FINAL) + Q_PROPERTY(QJSValue whatsThis READ whatsThis WRITE setWhatsThis NOTIFY whatsThisChanged FINAL) + Q_PROPERTY(QJSValue setWhatsThis READ getSetWhatsThis WRITE setSetWhatsThis + NOTIFY setWhatsThisChanged FINAL) + Q_PROPERTY(QJSValue font READ font WRITE setFont NOTIFY fontChanged FINAL) + Q_PROPERTY(QJSValue setFont READ getSetFont WRITE setSetFont NOTIFY setFontChanged FINAL) + Q_PROPERTY(QJSValue textAlignment READ textAlignment WRITE setTextAlignment + NOTIFY textAlignmentChanged FINAL) + Q_PROPERTY(QJSValue setTextAlignment READ getSetTextAlignment WRITE + setSetTextAlignment NOTIFY setTextAlignmentChanged FINAL) + Q_PROPERTY(QJSValue background READ background WRITE setBackground NOTIFY + backgroundChanged FINAL) + Q_PROPERTY(QJSValue setBackground READ getSetBackground WRITE + setSetBackground NOTIFY setBackgroundChanged FINAL) + Q_PROPERTY(QJSValue foreground READ foreground WRITE setForeground NOTIFY + foregroundChanged FINAL) + Q_PROPERTY(QJSValue setForeground READ getSetForeground WRITE + setSetForeground NOTIFY setForegroundChanged FINAL) + Q_PROPERTY(QJSValue checkState READ checkState WRITE setCheckState NOTIFY + checkStateChanged FINAL) + Q_PROPERTY(QJSValue setCheckState READ getSetCheckState WRITE + setSetCheckState NOTIFY setCheckStateChanged FINAL) + Q_PROPERTY(QJSValue accessibleText READ accessibleText WRITE + setAccessibleText NOTIFY accessibleTextChanged FINAL) + Q_PROPERTY(QJSValue setAccessibleText READ getSetAccessibleText WRITE + setSetAccessibleText NOTIFY setAccessibleTextChanged FINAL) + Q_PROPERTY(QJSValue accessibleDescription READ accessibleDescription + WRITE setAccessibleDescription NOTIFY accessibleDescriptionChanged FINAL) + Q_PROPERTY(QJSValue setAccessibleDescription READ getSetAccessibleDescription + WRITE setSetAccessibleDescription NOTIFY setAccessibleDescriptionChanged FINAL) + Q_PROPERTY(QJSValue sizeHint READ sizeHint WRITE setSizeHint NOTIFY sizeHintChanged FINAL) + Q_PROPERTY(QJSValue setSizeHint READ getSetSizeHint WRITE setSetSizeHint + NOTIFY setSizeHintChanged FINAL) + QML_NAMED_ELEMENT(TableFromListModelColumn) + public: + QmlTableFromListModelColumn(QObject* parent = nullptr); + ~QmlTableFromListModelColumn() override; + QJSValue display() const; + void setDisplay(const QJSValue& stringOrFunction); + QJSValue getSetDisplay() const; + void setSetDisplay(const QJSValue& function); + QJSValue decoration() const; + void setDecoration(const QJSValue& stringOrFunction); + QJSValue getSetDecoration() const; + void setSetDecoration(const QJSValue& function); + QJSValue edit() const; + void setEdit(const QJSValue& stringOrFunction); + QJSValue getSetEdit() const; + void setSetEdit(const QJSValue& function); + QJSValue toolTip() const; + void setToolTip(const QJSValue& stringOrFunction); + QJSValue getSetToolTip() const; + void setSetToolTip(const QJSValue& function); + QJSValue statusTip() const; + void setStatusTip(const QJSValue& stringOrFunction); + QJSValue getSetStatusTip() const; + void setSetStatusTip(const QJSValue& function); + QJSValue whatsThis() const; + void setWhatsThis(const QJSValue& stringOrFunction); + QJSValue getSetWhatsThis() const; + void setSetWhatsThis(const QJSValue& function); + QJSValue font() const; + void setFont(const QJSValue& stringOrFunction); + QJSValue getSetFont() const; + void setSetFont(const QJSValue& function); + QJSValue textAlignment() const; + void setTextAlignment(const QJSValue& stringOrFunction); + QJSValue getSetTextAlignment() const; + void setSetTextAlignment(const QJSValue& function); + QJSValue background() const; + void setBackground(const QJSValue& stringOrFunction); + QJSValue getSetBackground() const; + void setSetBackground(const QJSValue& function); + QJSValue foreground() const; + void setForeground(const QJSValue& stringOrFunction); + QJSValue getSetForeground() const; + void setSetForeground(const QJSValue& function); + QJSValue checkState() const; + void setCheckState(const QJSValue& stringOrFunction); + QJSValue getSetCheckState() const; + void setSetCheckState(const QJSValue& function); + QJSValue accessibleText() const; + void setAccessibleText(const QJSValue& stringOrFunction); + QJSValue getSetAccessibleText() const; + void setSetAccessibleText(const QJSValue& function); + QJSValue accessibleDescription() const; + void setAccessibleDescription(const QJSValue& stringOrFunction); + QJSValue getSetAccessibleDescription() const; + void setSetAccessibleDescription(const QJSValue& function); + QJSValue sizeHint() const; + void setSizeHint(const QJSValue& stringOrFunction); + QJSValue getSetSizeHint() const; + void setSetSizeHint(const QJSValue& function); + QJSValue getterAtRole(const QString& roleName); + QJSValue setterAtRole(const QString& roleName); + const QHash getters() const; + static const QHash supportedRoleNames(); + Q_SIGNALS: + void indexChanged(); + void displayChanged(); + void setDisplayChanged(); + void decorationChanged(); + void setDecorationChanged(); + void editChanged(); + void setEditChanged(); + void toolTipChanged(); + void setToolTipChanged(); + void statusTipChanged(); + void setStatusTipChanged(); + void whatsThisChanged(); + void setWhatsThisChanged(); + void fontChanged(); + void setFontChanged(); + void textAlignmentChanged(); + void setTextAlignmentChanged(); + void backgroundChanged(); + void setBackgroundChanged(); + void foregroundChanged(); + void setForegroundChanged(); + void checkStateChanged(); + void setCheckStateChanged(); + void accessibleTextChanged(); + void setAccessibleTextChanged(); + void accessibleDescriptionChanged(); + void setAccessibleDescriptionChanged(); + void sizeHintChanged(); + void setSizeHintChanged(); + + private: + // We store these in hashes because QQuickTableModel needs string-based lookup + // in certain situations. + QHash m_getters; + QHash m_setters; +}; + +} // namespace qml +} // namespace mixxx From d3f52590fce5e41085fcaa6549d64ee9b3186e11 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Thu, 30 Dec 2021 00:56:53 +0100 Subject: [PATCH 2/6] feat(qml): Use TableView for Library --- res/qml/Library.qml | 149 +++++++++++++++++++++++++------------------- 1 file changed, 84 insertions(+), 65 deletions(-) diff --git a/res/qml/Library.qml b/res/qml/Library.qml index 3f94320aac26..84bbb52232a4 100644 --- a/res/qml/Library.qml +++ b/res/qml/Library.qml @@ -1,5 +1,5 @@ import Mixxx 1.0 as Mixxx -import QtQuick 2.12 +import QtQuick import "Theme" Item { @@ -11,94 +11,117 @@ Item { id: libraryControl onMoveVertical: (offset) => { - listView.moveSelectionVertical(offset); + tableView.selectionModel.moveSelectionVertical(offset); } onLoadSelectedTrack: (group, play) => { - listView.loadSelectedTrack(group, play); + tableView.loadSelectedTrack(group, play); } onLoadSelectedTrackIntoNextAvailableDeck: (play) => { - listView.loadSelectedTrackIntoNextAvailableDeck(play); + tableView.loadSelectedTrackIntoNextAvailableDeck(play); } onFocusWidgetChanged: { switch (focusWidget) { case FocusedWidgetControl.WidgetKind.LibraryView: - listView.forceActiveFocus(); + tableView.forceActiveFocus(); break; } } } - ListView { - id: listView - - function moveSelectionVertical(value) { - if (value == 0) - return ; - - const rowCount = model.rowCount(); - if (rowCount == 0) - return ; - - currentIndex = Mixxx.MathUtils.positiveModulo(currentIndex + value, rowCount); - } + TableView { + id: tableView function loadSelectedTrackIntoNextAvailableDeck(play) { - const url = model.get(currentIndex).fileUrl; - if (!url) + const urls = this.selectionModel.selectedTrackUrls(); + if (urls.length == 0) return ; - Mixxx.PlayerManager.loadLocationUrlIntoNextAvailableDeck(url, play); + Mixxx.PlayerManager.loadLocationUrlIntoNextAvailableDeck(urls[0], play); } function loadSelectedTrack(group, play) { - const url = model.get(currentIndex).fileUrl; - if (!url) - return ; - - const player = Mixxx.PlayerManager.getPlayer(group); - if (!player) + const urls = this.selectionModel.selectedTrackUrls(); + if (urls.length == 0) return ; - player.loadTrackFromLocationUrl(url, play); + player.loadTrackFromLocationUrl(urls[0], play); } anchors.fill: parent - anchors.margins: 10 + anchors.margins: 5 clip: true - keyNavigationWraps: true - highlightMoveDuration: 250 - highlightResizeDuration: 50 - model: Mixxx.Library.model - Keys.onPressed: (event) => { - switch (event.key) { - case Qt.Key_Enter: - case Qt.Key_Return: - listView.loadSelectedTrackIntoNextAvailableDeck(false); - break; + focus: true + Keys.onUpPressed: this.selectionModel.moveSelectionVertical(-1) + Keys.onDownPressed: this.selectionModel.moveSelectionVertical(1) + Keys.onEnterPressed: this.loadSelectedTrackIntoNextAvailableDeck(false) + Keys.onReturnPressed: this.loadSelectedTrackIntoNextAvailableDeck(false) + + model: Mixxx.TableFromListModel { + sourceModel: Mixxx.Library.model + + Mixxx.TableFromListModelColumn { + display: "title" + decoration: "fileUrl" + } + + Mixxx.TableFromListModelColumn { + display: "artist" + decoration: "fileUrl" + } + + Mixxx.TableFromListModelColumn { + display: "album" + decoration: "fileUrl" + } + } + + selectionModel: ItemSelectionModel { + function selectRow(row) { + const rowCount = this.model.rowCount(); + if (rowCount == 0) { + this.clear(); + return ; + } + const newRow = Mixxx.MathUtils.positiveModulo(row, rowCount); + this.select(this.model.index(newRow, 0), ItemSelectionModel.Rows | ItemSelectionModel.Select | ItemSelectionModel.Clear | ItemSelectionModel.Current); + } + + function moveSelectionVertical(value) { + if (value == 0) + return ; + + const selected = this.selectedIndexes; + const oldRow = (selected.length == 0) ? 0 : selected[0].row; + this.selectRow(oldRow + value); + } + + function selectedTrackUrls() { + return this.selectedIndexes.map((index) => { + return this.model.sourceModel.get(index.row).fileUrl; + }); } + + model: tableView.model } delegate: Item { - id: itemDlgt + id: itemDelegate - required property int index - required property url fileUrl - required property string artist - required property string title + required property int row + required property int column + required property bool selected + required property string decoration + required property string display - implicitWidth: listView.width + implicitWidth: 300 implicitHeight: 30 Text { - anchors.verticalCenter: parent.verticalCenter - text: itemDlgt.artist + " - " + itemDlgt.title - color: (listView.currentIndex == itemDlgt.index && listView.activeFocus) ? Theme.blue : Theme.deckTextColor - - Behavior on color { - ColorAnimation { - duration: listView.highlightMoveDuration - } - } + anchors.fill: parent + text: display + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + color: itemDelegate.selected ? Theme.blue : Theme.white } Image { @@ -108,8 +131,8 @@ Item { Drag.dragType: Drag.Automatic Drag.supportedActions: Qt.CopyAction Drag.mimeData: { - "text/uri-list": itemDlgt.fileUrl, - "text/plain": itemDlgt.fileUrl + "text/uri-list": itemDelegate.decoration, + "text/plain": itemDelegate.decoration } anchors.fill: parent } @@ -120,21 +143,17 @@ Item { anchors.fill: parent drag.target: dragItem onPressed: { - listView.forceActiveFocus(); - listView.currentIndex = itemDlgt.index; + tableView.selectionModel.selectRow(itemDelegate.row); parent.grabToImage((result) => { dragItem.Drag.imageSource = result.url; }); } - onDoubleClicked: listView.loadSelectedTrackIntoNextAvailableDeck(false) + onDoubleClicked: { + tableView.selectionModel.selectRow(itemDelegate.row); + tableView.loadSelectedTrackIntoNextAvailableDeck(false); + } } } - - highlight: Rectangle { - border.color: listView.activeFocus ? Theme.blue : Theme.deckTextColor - border.width: 1 - color: "transparent" - } } } } From 3e9327473db3368a2b4cb93c5fcc060ddde1ee94 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 31 Dec 2021 00:04:39 +0100 Subject: [PATCH 3/6] chore(qml): Add support for more roles to QmlLibraryTrackListModel --- src/qml/qmllibrarytracklistmodel.cpp | 138 +++++++++++++++++++++++++-- src/qml/qmllibrarytracklistmodel.h | 29 +++++- 2 files changed, 155 insertions(+), 12 deletions(-) diff --git a/src/qml/qmllibrarytracklistmodel.cpp b/src/qml/qmllibrarytracklistmodel.cpp index 9678b5f34a1c..2dd4bd45e2d0 100644 --- a/src/qml/qmllibrarytracklistmodel.cpp +++ b/src/qml/qmllibrarytracklistmodel.cpp @@ -1,17 +1,50 @@ #include "qml/qmllibrarytracklistmodel.h" #include "library/librarytablemodel.h" +#include "qml/asyncimageprovider.h" namespace mixxx { namespace qml { namespace { const QHash kRoleNames = { - {QmlLibraryTrackListModel::TitleRole, "title"}, - {QmlLibraryTrackListModel::ArtistRole, "artist"}, - {QmlLibraryTrackListModel::AlbumRole, "album"}, {QmlLibraryTrackListModel::AlbumArtistRole, "albumArtist"}, + {QmlLibraryTrackListModel::AlbumRole, "album"}, + {QmlLibraryTrackListModel::ArtistRole, "artist"}, + {QmlLibraryTrackListModel::BitrateRole, "bitrate"}, + {QmlLibraryTrackListModel::BpmLockRole, "bpmLock"}, + {QmlLibraryTrackListModel::BpmRole, "bpm"}, + {QmlLibraryTrackListModel::ColorRole, "color"}, + {QmlLibraryTrackListModel::CommentRole, "comment"}, + {QmlLibraryTrackListModel::ComposerRole, "composer"}, + {QmlLibraryTrackListModel::CoverArtColorRole, "coverArtColor"}, + {QmlLibraryTrackListModel::CoverArtUrlRole, "coverArtUrl"}, + {QmlLibraryTrackListModel::DatetimeAddedRole, "datetimeAdded"}, + {QmlLibraryTrackListModel::DeletedRole, "deleted"}, + {QmlLibraryTrackListModel::DurationSecondsRole, "durationSeconds"}, + {QmlLibraryTrackListModel::FileTypeRole, "fileType"}, {QmlLibraryTrackListModel::FileUrlRole, "fileUrl"}, + {QmlLibraryTrackListModel::GenreRole, "genre"}, + {QmlLibraryTrackListModel::GroupingRole, "grouping"}, + {QmlLibraryTrackListModel::KeyIdRole, "keyIdRole"}, + {QmlLibraryTrackListModel::KeyRole, "key"}, + {QmlLibraryTrackListModel::LastPlayedAtRole, "lastPlayedAt"}, + {QmlLibraryTrackListModel::PlayedRole, "played"}, + {QmlLibraryTrackListModel::RatingRole, "rating"}, + {QmlLibraryTrackListModel::ReplayGainRole, "ReplayGain"}, + {QmlLibraryTrackListModel::TimesPlayedRole, "timesPlayed"}, + {QmlLibraryTrackListModel::TitleRole, "title"}, + {QmlLibraryTrackListModel::TrackNumberRole, "trackNumber"}, + {QmlLibraryTrackListModel::YearRole, "year"}, }; + +QColor colorFromRgbCode(double colorValue) { + if (colorValue < 0 || colorValue > 0xFFFFFF) { + return {}; + } + + QRgb rgbValue = static_cast(colorValue) | 0xFF000000; + return QColor(rgbValue); +} } QmlLibraryTrackListModel::QmlLibraryTrackListModel(LibraryTableModel* pModel, QObject* pParent) @@ -40,17 +73,68 @@ QVariant QmlLibraryTrackListModel::data(const QModelIndex& proxyIndex, int role) int column = -1; switch (role) { - case TitleRole: - column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE); + case AlbumArtistRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST); + break; + case AlbumRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM); break; case ArtistRole: column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST); break; - case AlbumRole: - column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM); + case BitrateRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE); break; - case AlbumArtistRole: - column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST); + case BpmLockRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK); + break; + case BpmRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM); + break; + case ColorRole: { + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COLOR); + const double colorValue = QIdentityProxyModel::data( + proxyIndex.siblingAtColumn(column), Qt::DisplayRole) + .toDouble(); + return colorFromRgbCode(colorValue); + break; + } + case CommentRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMMENT); + break; + case ComposerRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER); + break; + case CoverArtUrlRole: { + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_TRACKLOCATIONSTABLE_LOCATION); + const QString location = QIdentityProxyModel::data( + proxyIndex.siblingAtColumn(column), Qt::DisplayRole) + .toString(); + if (location.isEmpty()) { + return {}; + } + + return AsyncImageProvider::trackLocationToCoverArtUrl(location); + } + case CoverArtColorRole: { + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART_COLOR); + const double colorValue = QIdentityProxyModel::data( + proxyIndex.siblingAtColumn(column), Qt::DisplayRole) + .toDouble(); + return colorFromRgbCode(colorValue); + break; + } + case DatetimeAddedRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED); + break; + case DeletedRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_MIXXXDELETED); + break; + case DurationSecondsRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION); + break; + case FileTypeRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE); break; case FileUrlRole: { column = pSourceModel->fieldIndex(ColumnCache::COLUMN_TRACKLOCATIONSTABLE_LOCATION); @@ -62,6 +146,42 @@ QVariant QmlLibraryTrackListModel::data(const QModelIndex& proxyIndex, int role) } return QUrl::fromLocalFile(location); } + case GenreRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GENRE); + break; + case GroupingRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING); + break; + case KeyIdRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY_ID); + break; + case KeyRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY); + break; + case LastPlayedAtRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_LAST_PLAYED_AT); + break; + case PlayedRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PLAYED); + break; + case RatingRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING); + break; + case ReplayGainRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN); + break; + case TimesPlayedRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED); + break; + case TitleRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE); + break; + case TrackNumberRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER); + break; + case YearRole: + column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR); + break; default: break; } diff --git a/src/qml/qmllibrarytracklistmodel.h b/src/qml/qmllibrarytracklistmodel.h index 490ff4508c82..2f3d6a07278b 100644 --- a/src/qml/qmllibrarytracklistmodel.h +++ b/src/qml/qmllibrarytracklistmodel.h @@ -14,11 +14,34 @@ class QmlLibraryTrackListModel : public QIdentityProxyModel { public: enum Roles { - TitleRole = Qt::UserRole, - ArtistRole, + AlbumArtistRole = Qt::UserRole, AlbumRole, - AlbumArtistRole, + ArtistRole, + BitrateRole, + BpmLockRole, + BpmRole, + ColorRole, + CommentRole, + ComposerRole, + CoverArtColorRole, + CoverArtUrlRole, + DatetimeAddedRole, + DeletedRole, + DurationSecondsRole, + FileTypeRole, FileUrlRole, + GenreRole, + GroupingRole, + KeyIdRole, + KeyRole, + LastPlayedAtRole, + PlayedRole, + RatingRole, + ReplayGainRole, + TimesPlayedRole, + TitleRole, + TrackNumberRole, + YearRole, }; Q_ENUM(Roles); From a75289f02cdec315dc6b837a0b9a73013ffc1164 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 31 Dec 2021 00:05:09 +0100 Subject: [PATCH 4/6] feat(qml): Add more columns to library table --- res/qml/Library.qml | 171 +++++++++++++++++++++++++++++++++----------- 1 file changed, 129 insertions(+), 42 deletions(-) diff --git a/res/qml/Library.qml b/res/qml/Library.qml index 84bbb52232a4..3fec9de0b6c3 100644 --- a/res/qml/Library.qml +++ b/res/qml/Library.qml @@ -1,4 +1,5 @@ import Mixxx 1.0 as Mixxx +import Qt.labs.qmlmodels import QtQuick import "Theme" @@ -51,27 +52,73 @@ Item { anchors.margins: 5 clip: true focus: true + reuseItems: false Keys.onUpPressed: this.selectionModel.moveSelectionVertical(-1) Keys.onDownPressed: this.selectionModel.moveSelectionVertical(1) Keys.onEnterPressed: this.loadSelectedTrackIntoNextAvailableDeck(false) Keys.onReturnPressed: this.loadSelectedTrackIntoNextAvailableDeck(false) + columnWidthProvider: function(column) { + switch (column) { + case 0: + return 30; + case 1: + return 50; + case 2: + case 3: + case 4: + return 300; + default: + return 100; + } + } model: Mixxx.TableFromListModel { sourceModel: Mixxx.Library.model Mixxx.TableFromListModelColumn { - display: "title" - decoration: "fileUrl" + decoration: "color" + edit: "fileUrl" + } + + Mixxx.TableFromListModelColumn { + display: "coverArtUrl" + decoration: "coverArtColor" + edit: "fileUrl" } Mixxx.TableFromListModelColumn { display: "artist" - decoration: "fileUrl" + edit: "fileUrl" } Mixxx.TableFromListModelColumn { display: "album" - decoration: "fileUrl" + edit: "fileUrl" + } + + Mixxx.TableFromListModelColumn { + display: "year" + edit: "fileUrl" + } + + Mixxx.TableFromListModelColumn { + display: "bpm" + edit: "fileUrl" + } + + Mixxx.TableFromListModelColumn { + display: "key" + edit: "fileUrl" + } + + Mixxx.TableFromListModelColumn { + display: "fileType" + edit: "fileUrl" + } + + Mixxx.TableFromListModelColumn { + display: "bitrate" + edit: "fileUrl" } } @@ -104,53 +151,93 @@ Item { model: tableView.model } - delegate: Item { - id: itemDelegate + delegate: DelegateChooser { + DelegateChoice { + column: 0 - required property int row - required property int column - required property bool selected - required property string decoration - required property string display + Rectangle { + id: trackColorDelegate - implicitWidth: 300 - implicitHeight: 30 + required property bool selected + required property color decoration - Text { - anchors.fill: parent - text: display - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - color: itemDelegate.selected ? Theme.blue : Theme.white + implicitWidth: 30 + implicitHeight: 30 + color: trackColorDelegate.decoration + } } - Image { - id: dragItem + DelegateChoice { + column: 1 - Drag.active: dragArea.drag.active - Drag.dragType: Drag.Automatic - Drag.supportedActions: Qt.CopyAction - Drag.mimeData: { - "text/uri-list": itemDelegate.decoration, - "text/plain": itemDelegate.decoration - } - anchors.fill: parent - } + Rectangle { + id: coverArtDelegate - MouseArea { - id: dragArea + required property color decoration + required property url display - anchors.fill: parent - drag.target: dragItem - onPressed: { - tableView.selectionModel.selectRow(itemDelegate.row); - parent.grabToImage((result) => { - dragItem.Drag.imageSource = result.url; - }); + implicitWidth: 60 + implicitHeight: 30 + color: coverArtDelegate.decoration + + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: coverArtDelegate.display + clip: true + asynchronous: true + } } - onDoubleClicked: { - tableView.selectionModel.selectRow(itemDelegate.row); - tableView.loadSelectedTrackIntoNextAvailableDeck(false); + } + + DelegateChoice { + Item { + id: itemDelegate + + required property int row + required property bool selected + required property string display + + implicitWidth: 300 + implicitHeight: 30 + + Text { + anchors.fill: parent + text: itemDelegate.display + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + color: itemDelegate.selected ? Theme.blue : Theme.white + } + + Image { + id: dragItem + + Drag.active: dragArea.drag.active + Drag.dragType: Drag.Automatic + Drag.supportedActions: Qt.CopyAction + Drag.mimeData: { + "text/uri-list": edit, + "text/plain": edit + } + anchors.fill: parent + } + + MouseArea { + id: dragArea + + anchors.fill: parent + drag.target: dragItem + onPressed: { + tableView.selectionModel.selectRow(itemDelegate.row); + parent.grabToImage((result) => { + dragItem.Drag.imageSource = result.url; + }); + } + onDoubleClicked: { + tableView.selectionModel.selectRow(itemDelegate.row); + tableView.loadSelectedTrackIntoNextAvailableDeck(false); + } + } } } } From b407ed4345fcf45786289db50399e3ea91389489 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Mon, 7 Feb 2022 21:52:52 +0100 Subject: [PATCH 5/6] feat(qml): Add table headers to library --- res/qml/Library.qml | 72 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/res/qml/Library.qml b/res/qml/Library.qml index 3fec9de0b6c3..224e1eb719c6 100644 --- a/res/qml/Library.qml +++ b/res/qml/Library.qml @@ -1,6 +1,7 @@ import Mixxx 1.0 as Mixxx import Qt.labs.qmlmodels import QtQuick +import QtQuick.Controls 2.15 import "Theme" Item { @@ -29,6 +30,72 @@ Item { } } + HorizontalHeaderView { + id: horizontalHeader + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 5 + syncView: tableView + model: ["Color", "Cover", "Artist", "Album", "Year", "Bpm", "Key", "Filetype", "Bitrate"] + + delegate: Item { + id: headerDlgt + + required property int column + required property string modelData + + implicitHeight: columnName.contentHeight + 5 + implicitWidth: columnName.contentWidth + 5 + + BorderImage { + anchors.fill: parent + horizontalTileMode: BorderImage.Stretch + verticalTileMode: BorderImage.Stretch + source: Theme.imgPopupBackground + + border { + top: 10 + left: 20 + right: 20 + bottom: 10 + } + } + + Text { + id: columnName + + text: headerDlgt.modelData + anchors.fill: parent + anchors.margins: 5 + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.family: Theme.fontFamily + font.capitalization: Font.AllUppercase + font.bold: true + font.pixelSize: Theme.buttonFontPixelSize + color: Theme.buttonNormalColor + } + + Text { + id: sortIndicator + + anchors.fill: parent + anchors.margins: 5 + elide: Text.ElideRight + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + font.family: Theme.fontFamily + font.capitalization: Font.AllUppercase + font.bold: true + font.pixelSize: Theme.buttonFontPixelSize + color: Theme.buttonNormalColor + } + } + } + TableView { id: tableView @@ -48,7 +115,10 @@ Item { player.loadTrackFromLocationUrl(urls[0], play); } - anchors.fill: parent + anchors.top: horizontalHeader.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom anchors.margins: 5 clip: true focus: true From 228085733a52a637f0903cb4c31756060f2a42c6 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Thu, 10 Mar 2022 14:37:43 +0100 Subject: [PATCH 6/6] chore(qml): Forward signals in `QmlTableFromListModel` --- src/qml/qmltablefromlistmodel.cpp | 75 +++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/qml/qmltablefromlistmodel.cpp b/src/qml/qmltablefromlistmodel.cpp index e6fdf4c40866..ad294377571a 100644 --- a/src/qml/qmltablefromlistmodel.cpp +++ b/src/qml/qmltablefromlistmodel.cpp @@ -51,6 +51,81 @@ void QmlTableFromListModel::setSourceModel(QAbstractItemModel* pSourceModel) { index(sourceBottomRight.row(), columnCount()); emit dataChanged(topLeft, bottomRight); }); + connect(pSourceModel, + &QAbstractListModel::rowsAboutToBeInserted, + this, + [this](const QModelIndex& parent, + int start, + int end) { + DEBUG_ASSERT(!parent.isValid()); + beginInsertRows(QModelIndex(), start, end); + }); + connect(pSourceModel, + &QAbstractListModel::rowsInserted, + this, + [this](const QModelIndex& parent, + int start, + int end) { + qWarning() << "inserted" << start << end; + DEBUG_ASSERT(!parent.isValid()); + Q_UNUSED(parent); + Q_UNUSED(start); + Q_UNUSED(end); + endInsertRows(); + }); + connect(pSourceModel, + &QAbstractListModel::rowsAboutToBeMoved, + this, + [this](const QModelIndex& sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex& destinationParent, + int destinationChild) { + DEBUG_ASSERT(!sourceParent.isValid()); + DEBUG_ASSERT(!destinationParent.isValid()); + beginMoveRows(QModelIndex(), + sourceStart, + sourceEnd, + destinationParent, + destinationChild); + }); + connect(pSourceModel, + &QAbstractListModel::rowsMoved, + this, + [this](const QModelIndex& sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex& destinationParent, + int destinationChild) { + DEBUG_ASSERT(!sourceParent.isValid()); + Q_UNUSED(sourceParent); + Q_UNUSED(sourceStart); + Q_UNUSED(sourceEnd); + Q_UNUSED(destinationParent); + Q_UNUSED(destinationChild); + endMoveRows(); + }); + connect(pSourceModel, + &QAbstractListModel::rowsAboutToBeRemoved, + this, + [this](const QModelIndex& parent, + int start, + int end) { + DEBUG_ASSERT(!parent.isValid()); + beginRemoveRows(QModelIndex(), start, end); + }); + connect(pSourceModel, + &QAbstractListModel::rowsRemoved, + this, + [this](const QModelIndex& parent, + int start, + int end) { + DEBUG_ASSERT(!parent.isValid()); + Q_UNUSED(parent); + Q_UNUSED(start); + Q_UNUSED(end); + endRemoveRows(); + }); } emit sourceModelChanged(); endResetModel();