Commit 669becc3 authored by Nate Graham's avatar Nate Graham
Browse files

Extract common DataGridView and DataListView code into AbstractDataView

Even after recent refactors, DataGridView and DataListView still had
a huge amount of shared boilerplate that needed to be kept in sync.

This commit goes a step further by separating out everything common
into a new AbstractDataView component that both of them inherit from.
Now each one only contains code that is unique to it, which means that
the shared view layout and glue code only live in one place.
parent da4ce186
......@@ -410,6 +410,7 @@ if (Qt5Quick_FOUND AND Qt5Widgets_FOUND)
qml/ContextView.qml
qml/ContentView.qml
qml/ViewSelector.qml
qml/AbstractDataView.qml
qml/DataGridView.qml
qml/DataListView.qml
qml/DurationSlider.qml
......
/*
SPDX-FileCopyrightText: 2018 (c) Matthieu Gallien <matthieu_gallien@yahoo.fr>
SPDX-FileCopyrightText: 2022 (c) Nate Graham <kate@kde.org>
SPDX-License-Identifier: LGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQml.Models 2.1
import QtQuick.Layouts 1.2
import org.kde.kirigami 2.12 as Kirigami
import org.kde.elisa 1.0
FocusScope {
id: abstractView
// Subclasses must set these
property alias delegate: delegateModel.delegate
property alias contentView: scrollView.contentItem
// Instances should set these as needed
property AbstractItemModel realModel
property AbstractProxyModel proxyModel
property var modelType
property var filterType
property var filter
property string mainTitle
property string secondaryTitle
property url image
property bool sortModel: true
property bool isSubPage: false
property bool haveTreeModel: false
property bool modelIsInitialized: false
property bool allowArtistNavigation: false
property bool showCreateRadioButton: false
property bool showEnqueueButton: false
property bool displaySingleAlbum: false
property alias showRating: navigationBar.showRating
property alias sortRole: navigationBar.sortRole
property alias sortRoles: navigationBar.sortRoles
property alias sortRoleNames: navigationBar.sortRoleNames
property alias sortOrderNames: navigationBar.sortOrderNames
property alias sortOrder: navigationBar.sortOrder
property alias viewManager: navigationBar.viewManager
property alias expandedFilterView: navigationBar.expandedFilterView
// Inner items exposed to subclasses for various purposes
readonly property alias delegateModel: delegateModel
readonly property alias navigationBar: navigationBar
readonly property int viewWidth: scrollView.width - scrollView.scrollBarWidth
// Other properties
property AbstractProxyModel contentModel
property int depth: 1
focus: true
Accessible.role: Accessible.Pane
Accessible.name: mainTitle
function initializeModel()
{
if (!proxyModel) {
return
}
if (!realModel) {
return
}
if (!ElisaApplication.musicManager) {
return
}
if (modelIsInitialized) {
return
}
proxyModel.sourceModel = realModel
proxyModel.dataType = modelType
proxyModel.playList = Qt.binding(function() { return ElisaApplication.mediaPlayListProxyModel })
abstractView.contentModel = proxyModel
if (sortModel) {
proxyModel.sortModel(sortOrder)
}
realModel.initializeByData(ElisaApplication.musicManager, ElisaApplication.musicManager.viewDatabase,
modelType, filterType, filter)
modelIsInitialized = true
}
function goToBack() {
if (haveTreeModel) {
delegateModel.rootIndex = delegateModel.parentModelIndex()
--depth
} else {
viewManager.goBack()
}
}
// Model
DelegateModel {
id: delegateModel
model: abstractView.contentModel
}
// Main view components
ColumnLayout {
anchors.fill: parent
spacing: 0
NavigationActionBar {
id: navigationBar
z: 1 // on top of track list
mainTitle: abstractView.mainTitle
secondaryTitle: abstractView.secondaryTitle
image: abstractView.image
enableGoBack: abstractView.isSubPage || abstractView.depth > 1
allowArtistNavigation: abstractView.isSubPage
showCreateRadioButton: abstractView.modelType === ElisaUtils.Radio
showEnqueueButton: abstractView.modelType !== ElisaUtils.Radio
Layout.fillWidth: true
Binding {
target: abstractView.contentModel
property: 'filterText'
when: abstractView.contentModel
value: navigationBar.filterText
}
Binding {
target: abstractView.contentModel
property: 'filterRating'
when: abstractView.contentModel
value: navigationBar.filterRating
}
Binding {
target: abstractView.contentModel
property: 'sortRole'
when: abstractView.contentModel && navigationBar.enableSorting
value: navigationBar.sortRole
}
onEnqueue: contentModel.enqueueToPlayList(delegateModel.rootIndex)
onReplaceAndPlay:contentModel.replaceAndPlayOfPlayList(delegateModel.rootIndex)
onGoBack: {
abstractView.goToBack()
}
onSortOrderChanged: {
if (!contentModel || !navigationBar.enableSorting) {
return
}
if ((contentModel.sortedAscending && sortOrder !== Qt.AscendingOrder) ||
(!contentModel.sortedAscending && sortOrder !== Qt.DescendingOrder)) {
contentModel.sortModel(sortOrder)
}
}
}
ScrollView {
id: scrollView
readonly property int scrollBarWidth: ScrollBar.vertical.visible ? ScrollBar.vertical.width : 0
Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.smallSpacing
// HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
// Content view goes here, set as the contentItem: property
}
}
// Loading spinner
Loader {
id: busyIndicatorLoader
anchors.centerIn: parent
height: Kirigami.Units.gridUnit * 5
width: height
active: realModel ? realModel.isBusy : true
visible: active && status == Loader.Ready
sourceComponent: BusyIndicator {
anchors.centerIn: parent
}
}
// "Nothing here" placeholder message
Loader {
anchors.centerIn: parent
width: parent.width - (Kirigami.Units.largeSpacing * 4)
active: contentDirectoryView.count === 0 && !busyIndicatorLoader.active
visible: status == Loader.Ready
sourceComponent: Kirigami.PlaceholderMessage {
anchors.centerIn: parent
text: i18n("Nothing to display")
}
}
Connections {
target: ElisaApplication
function onMusicManagerChanged() {
initializeModel()
}
}
Component.onCompleted: {
initializeModel()
}
}
......@@ -6,44 +6,15 @@
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQml.Models 2.1
import QtQuick.Layouts 1.2
import org.kde.kirigami 2.12 as Kirigami
import org.kde.elisa 1.0
FocusScope {
AbstractDataView {
id: gridView
property AbstractItemModel realModel
property AbstractProxyModel contentModel
property AbstractProxyModel proxyModel
property string mainTitle
property string secondaryTitle
property url image
property url defaultIcon
property int depth: 1
property bool isSubPage: false
property bool delegateDisplaySecondaryText: true
property bool haveTreeModel: false
property bool modelIsInitialized: false
property var filterType
property var modelType
property var filter
property alias expandedFilterView: navigationBar.expandedFilterView
property alias showRating: navigationBar.showRating
property alias sortRole: navigationBar.sortRole
property alias sortRoles: navigationBar.sortRoles
property alias sortRoleNames: navigationBar.sortRoleNames
property alias sortOrderNames: navigationBar.sortOrderNames
property alias sortOrder: navigationBar.sortOrder
property alias viewManager: navigationBar.viewManager
signal enqueue(var fullData, string name)
signal replaceAndPlay(var fullData, string name)
......@@ -59,245 +30,80 @@ FocusScope {
onOpen: viewManager.openChildView(fullData)
focus: true
Accessible.role: Accessible.Pane
Accessible.name: mainTitle
delegate: GridBrowserDelegate {
width: Kirigami.Settings.isMobile ? contentDirectoryView.cellWidth : elisaTheme.gridDelegateSize
height: contentDirectoryView.cellHeight
function initializeModel()
{
if (!proxyModel) {
return
}
focus: true
if (!realModel) {
return
}
isSelected: contentDirectoryView.currentIndex === index
if (!ElisaApplication.musicManager) {
return
}
isPartial: false
if (modelIsInitialized) {
return
}
proxyModel.sourceModel = realModel
proxyModel.dataType = modelType
proxyModel.playList = Qt.binding(function() { return ElisaApplication.mediaPlayListProxyModel })
gridView.contentModel = proxyModel
proxyModel.sortModel(sortOrder)
realModel.initializeByData(ElisaApplication.musicManager, ElisaApplication.musicManager.viewDatabase,
modelType, filterType, filter)
modelIsInitialized = true
}
function goToBack() {
if (haveTreeModel) {
delegateModel.rootIndex = delegateModel.parentModelIndex()
--depth
} else {
viewManager.goBack()
}
}
mainText: model.display
fileUrl: model.url ? model.url : ""
secondaryText: gridView.delegateDisplaySecondaryText && model.secondaryText ? model.secondaryText : ""
imageUrl: model.imageUrl ? model.imageUrl : ''
imageFallbackUrl: defaultIcon
databaseId: model.databaseId
delegateDisplaySecondaryText: gridView.delegateDisplaySecondaryText
entryType: model.dataType
hasChildren: model.hasChildren
// Model
DelegateModel {
id: delegateModel
model: gridView.contentModel
delegate: GridBrowserDelegate {
width: Kirigami.Settings.isMobile ? contentDirectoryView.cellWidth : elisaTheme.gridDelegateSize
height: contentDirectoryView.cellHeight
focus: true
isSelected: contentDirectoryView.currentIndex === index
isPartial: false
mainText: model.display
fileUrl: model.url ? model.url : ""
secondaryText: gridView.delegateDisplaySecondaryText && model.secondaryText ? model.secondaryText : ""
imageUrl: model.imageUrl ? model.imageUrl : ''
imageFallbackUrl: defaultIcon
databaseId: model.databaseId
delegateDisplaySecondaryText: gridView.delegateDisplaySecondaryText
entryType: model.dataType
hasChildren: model.hasChildren
onEnqueue: gridView.enqueue(model.fullData, model.display)
onReplaceAndPlay: gridView.replaceAndPlay(model.fullData, model.display)
onOpen: {
if (haveTreeModel && !model.hasModelChildren) {
return
}
if (haveTreeModel) {
delegateModel.rootIndex = delegateModel.modelIndex(model.index)
++depth
} else {
gridView.open(model.fullData)
}
}
onSelected: {
forceActiveFocus()
contentDirectoryView.currentIndex = model.index
onEnqueue: gridView.enqueue(model.fullData, model.display)
onReplaceAndPlay: gridView.replaceAndPlay(model.fullData, model.display)
onOpen: {
if (haveTreeModel && !model.hasModelChildren) {
return
}
onActiveFocusChanged: {
if (activeFocus && contentDirectoryView.currentIndex !== model.index) {
contentDirectoryView.currentIndex = model.index
}
if (haveTreeModel) {
delegateModel.rootIndex = delegateModel.modelIndex(model.index)
++depth
} else {
gridView.open(model.fullData)
}
}
}
// Main view components
ColumnLayout {
anchors.fill: parent
spacing: 0
NavigationActionBar {
id: navigationBar
z: 1 // on top of track list
mainTitle: gridView.mainTitle
secondaryTitle: gridView.secondaryTitle
image: gridView.image
enableGoBack: gridView.isSubPage || depth > 1
Layout.fillWidth: true
Binding {
target: gridView.contentModel
property: 'filterText'
when: gridView.contentModel
value: navigationBar.filterText
}
Binding {
target: gridView.contentModel
property: 'filterRating'
when: gridView.contentModel
value: navigationBar.filterRating
}
Binding {
target: gridView.contentModel
property: 'sortRole'
when: gridView.contentModel && navigationBar.enableSorting
value: navigationBar.sortRole
}
onEnqueue: contentModel.enqueueToPlayList(delegateModel.rootIndex)
onReplaceAndPlay:contentModel.replaceAndPlayOfPlayList(delegateModel.rootIndex)
onGoBack: {
gridView.goToBack()
}
onSortOrderChanged: {
if (!contentModel || !navigationBar.enableSorting) {
return
}
if ((contentModel.sortedAscending && sortOrder !== Qt.AscendingOrder) ||
(!contentModel.sortedAscending && sortOrder !== Qt.DescendingOrder)) {
contentModel.sortModel(sortOrder)
}
}
onSelected: {
forceActiveFocus()
contentDirectoryView.currentIndex = model.index
}
ScrollView {
id: scrollView
readonly property int scrollBarWidth: ScrollBar.vertical.visible ? ScrollBar.vertical.width : 0
readonly property int availableSpace: scrollView.width - scrollView.scrollBarWidth
Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.smallSpacing
// HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
contentItem: GridView {
id: contentDirectoryView
activeFocusOnTab: true
keyNavigationEnabled: true
reuseItems: true
model: delegateModel
// HACK: setting currentIndex to -1 in mobile for some reason causes segfaults, no idea why
currentIndex: Kirigami.Settings.isMobile ? 0 : -1
Accessible.role: Accessible.List
Accessible.name: mainTitle
cellWidth: {
let columns = Math.max(Math.floor(scrollView.availableSpace / elisaTheme.gridDelegateSize), 2);
return Math.floor(scrollView.availableSpace / columns);
}
cellHeight: {
if (Kirigami.Settings.isMobile) {
return cellWidth + Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing;
} else {
return elisaTheme.gridDelegateSize + Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing;
}
}
onActiveFocusChanged: {
if (activeFocus && contentDirectoryView.currentIndex !== model.index) {
contentDirectoryView.currentIndex = model.index
}
}
}
// Placeholder spinner while loading
Loader {
id: busyIndicatorLoader
anchors.centerIn: parent
height: Kirigami.Units.gridUnit * 5
width: height
visible: realModel ? realModel.isBusy : true
active: realModel ? realModel.isBusy : true
contentView: GridView {
id: contentDirectoryView
sourceComponent: BusyIndicator {
anchors.centerIn: parent
}
}
activeFocusOnTab: true
keyNavigationEnabled: true
reuseItems: true
// "Nothing here" placeholder message
Loader {
anchors.centerIn: parent
width: parent.width - (Kirigami.Units.largeSpacing * 4)
active: contentDirectoryView.count === 0 && !busyIndicatorLoader.active
sourceComponent: Kirigami.PlaceholderMessage {
anchors.centerIn: parent
text: i18n("Nothing to display")
}
}
model: delegateModel
// HACK: setting currentIndex to -1 in mobile for some reason causes segfaults, no idea why
currentIndex: Kirigami.Settings.isMobile ? 0 : -1
Connections {
target: ElisaApplication
Accessible.role: Accessible.List
Accessible.name: mainTitle
function onMusicManagerChanged() {
initializeModel()
cellWidth: {
let columns = Math.max(Math.floor(gridView.viewWidth / elisaTheme.gridDelegateSize), 2);
return Math.floor(gridView.viewWidth / columns);
}
cellHeight: {
if (Kirigami.Settings.isMobile) {
return cellWidth + Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing;
} else {
return elisaTheme.gridDelegateSize + Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing;
}
}
}
Component.onCompleted: {
initializeModel()
}
}
......@@ -6,48 +6,19 @@
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQml.Models 2.1
import QtQuick.Layouts 1.2
import org.kde.kirigami 2.12 as Kirigami
import org.kde.elisa 1.0
FocusScope {
AbstractDataView {
id: listView
property AbstractItemModel realModel
property AbstractProxyModel contentModel
property AbstractProxyModel proxyModel
property string mainTitle
property string secondaryTitle
property url image
property int depth: 1
property int databaseId
property bool showSection: false
property bool isSubPage: false
property bool haveTreeModel: false
property bool radioCase: false
property bool displaySingleAlbum: false
property bool modelIsInitialized: false
property var filter
property var modelType
property var filterType
property alias currentIndex: contentDirectoryView.currentIndex