mirror of https://github.com/KDE/kasts.git
Add selection, navigation and context menus to FeedListPage
This commit is contained in:
parent
a141cda44a
commit
c84d8ed47f
|
@ -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;"));
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue