Add selection, navigation and context menus to FeedListPage

This commit is contained in:
Bart De Vries 2021-09-19 21:27:26 +02:00
parent a141cda44a
commit c84d8ed47f
6 changed files with 309 additions and 52 deletions

View File

@ -17,7 +17,7 @@
#include "models/entriesmodel.h"
Feed::Feed(const QString &feedurl)
: QObject(nullptr)
: QObject(&DataManager::instance())
{
QSqlQuery query;
query.prepare(QStringLiteral("SELECT * FROM Feeds WHERE url=:feedurl;"));

View File

@ -32,9 +32,12 @@ FeedsModel::FeedsModel(QObject *parent)
QHash<int, QByteArray> FeedsModel::roleNames() const
{
QHash<int, QByteArray> roleNames;
roleNames[0] = "feed";
return roleNames;
return {
{FeedRole, "feed"},
{UrlRole, "url"},
{TitleRole, "title"},
{UnreadCountRole, "unreadCount"},
};
}
int FeedsModel::rowCount(const QModelIndex &parent) const
@ -45,19 +48,22 @@ int FeedsModel::rowCount(const QModelIndex &parent) const
QVariant FeedsModel::data(const QModelIndex &index, int role) const
{
if (role != 0)
switch (role) {
case FeedRole:
return QVariant::fromValue(DataManager::instance().getFeed(index.row()));
case UrlRole:
return QVariant::fromValue(DataManager::instance().getFeed(index.row())->url());
case TitleRole:
return QVariant::fromValue(DataManager::instance().getFeed(index.row())->name());
case UnreadCountRole:
return QVariant::fromValue(DataManager::instance().getFeed(index.row())->unreadEntryCount());
default:
return QVariant();
return QVariant::fromValue(DataManager::instance().getFeed(index.row()));
}
}
void FeedsModel::removeFeed(int index)
// Hack to get a QItemSelection in QML
QItemSelection FeedsModel::createSelection(int rowa, int rowb)
{
DataManager::instance().removeFeed(index);
}
void FeedsModel::refreshAll()
{
// for (auto &feed : m_feeds) {
// feed->refresh();
// }
return QItemSelection(index(rowa, 0), index(rowb, 0));
}

View File

@ -9,6 +9,7 @@
#include <QAbstractListModel>
#include <QHash>
#include <QItemSelection>
#include <QSqlTableModel>
#include <QUrl>
@ -19,10 +20,18 @@ class FeedsModel : public QAbstractListModel
Q_OBJECT
public:
enum Roles {
FeedRole = Qt::UserRole,
UrlRole,
TitleRole,
UnreadCountRole,
};
Q_ENUM(Roles)
explicit FeedsModel(QObject *parent = nullptr);
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent) const override;
Q_INVOKABLE void removeFeed(int index);
Q_INVOKABLE void refreshAll();
Q_INVOKABLE QItemSelection createSelection(int rowa, int rowb);
};

View File

@ -59,13 +59,17 @@ Kirigami.ScrollablePage {
contextualActions: [
Kirigami.Action {
iconName: "help-about-symbolic"
text: i18n("Details")
text: i18n("Podcast Details")
onTriggered: {
while(pageStack.depth > 2)
pageStack.pop()
pageStack.push("qrc:/FeedDetailsPage.qml", {"feed": feed})
}
},
}
/* Remove this action for now; there are already actions on the FeedListPage
* and through context menus; it's confusing to mix it with actions on
* entries.
,
Kirigami.Action {
iconName: "delete"
text: i18n("Remove Podcast")
@ -74,7 +78,7 @@ Kirigami.ScrollablePage {
pageStack.pop()
DataManager.removeFeed(feed)
}
}
}*/
]
// add the default actions through onCompleted to add them to the ones

View File

@ -9,6 +9,7 @@ import QtQuick 2.14
import QtQuick.Controls 2.14 as Controls
import QtQuick.Layouts 1.14
import QtGraphicalEffects 1.15
import QtQml.Models 2.15
import org.kde.kirigami 2.12 as Kirigami
@ -19,50 +20,126 @@ Controls.ItemDelegate {
required property int cardSize
required property int cardMargin
property int borderWidth: 1
implicitWidth: cardSize + 2 * cardMargin
implicitHeight: cardSize + 2 * cardMargin
property QtObject listView: undefined
property bool selected: false
property int row: model ? model.row : -1
property var activeBackgroundColor: Qt.lighter(Kirigami.Theme.highlightColor, 1.3)
highlighted: selected
Accessible.role: Accessible.Button
Accessible.name: feed.name
Accessible.onPressAction: {
feedDelegate.clicked();
}
onClicked: {
lastFeed = feed.url
if (pageStack.depth > 1)
pageStack.pop();
pageStack.push("qrc:/EntryListPage.qml", {"feed": feed})
clicked();
}
Keys.onReturnPressed: clicked()
background: Kirigami.ShadowedRectangle {
anchors.fill: parent
anchors.margins: cardMargin
anchors.leftMargin: cardMargin
color: Kirigami.Theme.backgroundColor
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.largeSpacing
shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.15)
shadow.yOffset: borderWidth * 2
border.width: borderWidth
border.color: Qt.tint(Kirigami.Theme.textColor,
Qt.rgba(color.r, color.g, color.b, 0.6))
// We need to update the "selected" status:
// - if the selected indexes changes
// - if our delegate moves
// - if the model moves and the delegate stays in the same place
function updateIsSelected() {
selected = listView.selectionModel.rowIntersectsSelection(row);
}
contentItem: Item {
onRowChanged: {
updateIsSelected();
}
Component.onCompleted: {
updateIsSelected();
}
background: Rectangle {
// Background for highlighted / hovered / active items
Rectangle {
id: background
anchors.fill: parent
color: feedDelegate.checked || feedDelegate.highlighted || (feedDelegate.supportsMouseEvents && feedDelegate.pressed)
? feedDelegate.activeBackgroundColor
: Kirigami.Theme.backgroundColor
Rectangle {
id: internal
property bool indicateActiveFocus: feedDelegate.pressed || Kirigami.Settings.tabletMode || feedDelegate.activeFocus || (feedDelegate.ListView.view ? feedDelegate.ListView.view.activeFocus : false)
anchors.fill: parent
visible: !Kirigami.Settings.tabletMode && feedDelegate.hoverEnabled
color: feedDelegate.activeBackgroundColor
opacity: (feedDelegate.hovered || feedDelegate.highlighted || feedDelegate.activeFocus) && !feedDelegate.pressed ? 0.5 : 0
}
}
Kirigami.ShadowedRectangle {
anchors.fill: parent
anchors.margins: cardMargin
anchors.leftMargin: cardMargin
color: Kirigami.Theme.backgroundColor
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.largeSpacing
shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.15)
shadow.yOffset: borderWidth * 2
border.width: borderWidth
border.color: Qt.tint(Kirigami.Theme.textColor, Qt.rgba(color.r, color.g, color.b, 0.6))
}
}
contentItem: MouseArea {
id: mouseArea
anchors.fill: parent
anchors.margins: cardMargin + borderWidth
anchors.leftMargin: cardMargin + borderWidth
implicitWidth: cardSize - 2 * borderWidth
implicitHeight: cardSize - 2 * borderWidth
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
// Keep track of (currently) selected items
var modelIndex = feedDelegate.listView.model.index(index, 0);
if (feedDelegate.listView.selectionModel.isSelected(modelIndex) && mouse.button == Qt.RightButton) {
feedDelegate.listView.contextMenu.popup(null, mouse.x+1, mouse.y+1);
} else if (mouse.modifiers & Qt.ShiftModifier) {
// Have to take a detour through c++ since selecting large sets
// in QML is extremely slow
feedDelegate.listView.selectionModel.select(feedDelegate.listView.model.createSelection(modelIndex.row, feedDelegate.listView.selectionModel.currentIndex.row), ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows);
} else if (mouse.modifiers & Qt.ControlModifier) {
feedDelegate.listView.selectionModel.select(modelIndex, ItemSelectionModel.Toggle | ItemSelectionModel.Rows);
} else if (mouse.button == Qt.LeftButton) {
feedDelegate.listView.currentIndex = index;
feedDelegate.listView.selectionModel.setCurrentIndex(modelIndex, ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows);
feedDelegate.clicked();
} else if (mouse.button == Qt.RightButton) {
// This item is right-clicked, but isn't selected
feedDelegate.listView.selectionForContextMenu = [modelIndex];
feedDelegate.listView.contextMenu.popup(null, mouse.x+1, mouse.y+1);
}
}
onPressAndHold: {
var modelIndex = feedDelegate.listView.model.index(index, 0);
feedDelegate.listView.selectionModel.select(modelIndex, ItemSelectionModel.Toggle | ItemSelectionModel.Rows);
}
Connections {
target: listView.selectionModel
function onSelectionChanged() {
updateIsSelected();
}
}
Connections {
target: listView.model
function onLayoutChanged() {
updateIsSelected();
}
}
ImageWithFallback {
id: img
anchors.fill: parent
@ -78,7 +155,7 @@ Controls.ItemDelegate {
anchors.right: img.right
width: actionsButton.width
height: actionsButton.height
color: Kirigami.Theme.highlightColor
color: feedDelegate.activeBackgroundColor
radius: Kirigami.Units.smallSpacing - 2 * borderWidth
}
@ -128,6 +205,13 @@ Controls.ItemDelegate {
}
}
onClicked: {
lastFeed = feed.url
if (pageStack.depth > 1)
pageStack.pop();
pageStack.push("qrc:/EntryListPage.qml", {"feed": feed})
}
Controls.ToolTip {
text: feed.name
delay: Qt.styleHints.mousePressAndHoldInterval

View File

@ -9,6 +9,7 @@ import QtQuick 2.14
import QtQuick.Controls 2.14 as Controls
import Qt.labs.platform 1.1
import QtQuick.Layouts 1.14
import QtQml.Models 2.15
import org.kde.kirigami 2.12 as Kirigami
@ -31,6 +32,14 @@ Kirigami.ScrollablePage {
}
}
actions.main: Kirigami.Action {
text: i18n("Add Podcast")
iconName: "list-add"
onTriggered: {
addSheet.open()
}
}
contextualActions: [
Kirigami.Action {
text: i18n("Refresh All Podcasts")
@ -50,11 +59,11 @@ Kirigami.ScrollablePage {
}
]
actions.main: Kirigami.Action {
text: i18n("Add Podcast")
iconName: "list-add"
onTriggered: {
addSheet.open()
// add the default actions through onCompleted to add them to the ones
// defined above
Component.onCompleted: {
for (var i in feedList.contextualActionList) {
contextualActions.push(feedList.contextualActionList[i]);
}
}
@ -115,11 +124,156 @@ Kirigami.ScrollablePage {
FeedListDelegate {
cardSize: feedList.availableWidth / feedList.columns - 2 * feedList.cardMargin
cardMargin: feedList.cardMargin
listView: feedList
}
}
delegate: Kirigami.DelegateRecycler {
sourceComponent: feedListDelegate
}
property var selectionForContextMenu: []
property ItemSelectionModel selectionModel: ItemSelectionModel {
id: selectionModel
model: feedList.model
onSelectionChanged: {
feedList.selectionForContextMenu = selectedIndexes;
}
}
// The selection is not updated when the model is reset, so we have to take
// this into account manually.
// TODO: Fix the fact that the current item is not highlighted after reset
Connections {
target: feedList.model
function onModelAboutToBeReset() {
selectionForContextMenu = [];
feedList.selectionModel.clear();
feedList.selectionModel.setCurrentIndex(model.index(0, 0), ItemSelectionModel.Current); // Only set current item; don't select it
currentIndex = 0;
}
}
Keys.onPressed: {
if (event.matches(StandardKey.SelectAll)) {
feedList.selectionModel.select(model.index(0, 0), ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Columns);
return;
}
switch (event.key) {
case Qt.Key_Left:
selectRelative(-1, event.modifiers == Qt.ShiftModifier);
return;
case Qt.Key_Right:
selectRelative(1, event.modifiers == Qt.ShiftModifier);
return;
case Qt.Key_Up:
selectRelative(-columns, event.modifiers == Qt.ShiftModifier);
return;
case Qt.Key_Down:
selectRelative(columns, event.modifiers == Qt.ShiftModifier);
return;
case Qt.Key_PageUp:
if (!atYBeginning) {
if ((contentY - feedList.height) < 0) {
contentY = 0
} else {
contentY -= feedList.height
}
returnToBounds()
}
return;
case Qt.Key_PageDown:
if (!atYEnd) {
if ((contentY + feedList.height) > contentHeight - height) {
contentY = contentHeight - height
} else {
contentY += feedList.height
}
returnToBounds()
}
return;
case Qt.Key_Home:
if (!atYBeginning) {
contentY = 0
returnToBounds()
}
return;
case Qt.Key_End:
if (!atYEnd) {
contentY = contentHeight - height
returnToBounds()
}
return;
default:
break;
}
}
onActiveFocusChanged: {
if (activeFocus && !selectionModel.hasSelection) {
selectionModel.clear();
selectionModel.setCurrentIndex(model.index(0, 0), ItemSelectionModel.Current); // Only set current item; don't select it
}
}
function selectRelative(delta, append) {
var nextRow = feedList.currentIndex + delta;
if (nextRow < 0) {
nextRow = feedList.currentIndex;
}
if (nextRow >= feedList.count) {
nextRow = feedList.currentIndex;
}
if (append) {
feedList.selectionModel.select(feedList.model.createSelection(nextRow, feedList.selectionModel.currentIndex.row), ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows);
} else {
feedList.selectionModel.setCurrentIndex(model.index(nextRow, 0), ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows);
}
}
// For lack of a better place, we put generic entry list actions here so
// they can be re-used across the different ListViews.
property var deleteFeedAction: Kirigami.Action {
iconName: "delete"
text: i18n("Remove Podcast")
visible: feedList.selectionModel.hasSelection
onTriggered: {
for (var i in feedList.selectionForContextMenu) {
DataManager.removeFeed(feedList.model.data(feedList.selectionForContextMenu[i], FeedsModel.FeedRole));
}
}
}
property var feedDetailsAction: Kirigami.Action {
iconName: "help-about-symbolic"
text: i18n("Podcast Details")
visible: feedList.selectionModel.hasSelection && (feedList.selectionForContextMenu.length == 1)
onTriggered: {
while(pageStack.depth > 1)
pageStack.pop()
pageStack.push("qrc:/FeedDetailsPage.qml", {"feed": feedList.selectionForContextMenu[0].model.data(feedList.selectionForContextMenu[0], FeedsModel.FeedRole)})
}
}
property var contextualActionList: [feedDetailsAction,
deleteFeedAction]
property Controls.Menu contextMenu: Controls.Menu {
id: contextMenu
Controls.MenuItem {
action: feedList.feedDetailsAction
visible: (feedList.selectionForContextMenu.length == 1)
height: visible ? implicitHeight : 0 // workaround for qqc2-breeze-style
}
Controls.MenuItem {
action: feedList.deleteFeedAction
visible: true
height: visible ? implicitHeight : 0 // workaround for qqc2-breeze-style
}
onClosed: {
// reset to normal selection if this context menu is closed
feedList.selectionForContextMenu = feedList.selectionModel.selectedIndexes;
}
}
}
}