Message model uses now cache, huge amount of related changes.

This commit is contained in:
martinrotter 2017-05-15 12:53:26 +02:00
parent 73925b6452
commit 118a99d49e
15 changed files with 397 additions and 241 deletions

View File

@ -2,6 +2,8 @@
—————
Added:
▪ Message list is now not reloaded when doing batch message operations.
▪ Message list SQL queries are now fully adjustable. This will allow for integration of labels functionality in the future.
▪ Auto-update status of feeds is now more general and complete. (issue #91)
▪ Display feed title in list of messages. (issue #97)
▪ Displayed feeds can now be sorted by multiple columns. Do you want to sort by author, and THEN by title? Simply click first "Title" column, then on "Author" column. If you hold CTRL during sorting, the sort is done in reverse column order.

View File

@ -329,7 +329,9 @@ HEADERS += src/core/feeddownloader.h \
src/miscellaneous/serviceoperator.h \
src/services/abstract/cacheforserviceroot.h \
src/services/tt-rss/gui/formeditttrssaccount.h \
src/gui/guiutilities.h
src/gui/guiutilities.h \
src/core/messagesmodelcache.h \
src/core/messagesmodelsqllayer.h
SOURCES += src/core/feeddownloader.cpp \
src/core/feedsmodel.cpp \
@ -450,7 +452,9 @@ SOURCES += src/core/feeddownloader.cpp \
src/miscellaneous/serviceoperator.cpp \
src/services/abstract/cacheforserviceroot.cpp \
src/services/tt-rss/gui/formeditttrssaccount.cpp \
src/gui/guiutilities.cpp
src/gui/guiutilities.cpp \
src/core/messagesmodelcache.cpp \
src/core/messagesmodelsqllayer.cpp
FORMS += src/gui/toolbareditor.ui \
src/network-web/downloaditem.ui \

View File

@ -24,41 +24,20 @@
#include "miscellaneous/iconfactory.h"
#include "miscellaneous/databasequeries.h"
#include "services/abstract/serviceroot.h"
#include "core/messagesmodelcache.h"
#include "services/abstract/recyclebin.h"
#include <QSqlField>
MessagesModel::MessagesModel(QObject *parent)
: QSqlTableModel(parent, qApp->database()->connection(QSL("MessagesModel"), DatabaseFactory::FromSettings)),
m_fieldNames(QMap<int,QString>()), m_sortColumn(QList<int>()), m_sortOrder(QList<Qt::SortOrder>()),
m_messageHighlighter(NoHighlighting), m_customDateFormat(QString()) {
: QSqlQueryModel(parent), MessagesModelSqlLayer(),
m_cache(new MessagesModelCache(this)), m_messageHighlighter(NoHighlighting), m_customDateFormat(QString()) {
setupFonts();
setupIcons();
setupHeaderData();
updateDateFormat();
m_fieldNames[MSG_DB_ID_INDEX] = "Messages.id";
m_fieldNames[MSG_DB_READ_INDEX] = "Messages.is_read";
m_fieldNames[MSG_DB_DELETED_INDEX] = "Messages.is_deleted";
m_fieldNames[MSG_DB_IMPORTANT_INDEX] = "Messages.is_important";
m_fieldNames[MSG_DB_FEED_TITLE_INDEX] = "Feeds.title";
m_fieldNames[MSG_DB_TITLE_INDEX] = "Messages.title";
m_fieldNames[MSG_DB_URL_INDEX] = "Messages.url";
m_fieldNames[MSG_DB_AUTHOR_INDEX] = "Messages.author";
m_fieldNames[MSG_DB_DCREATED_INDEX] = "Messages.date_created";
m_fieldNames[MSG_DB_CONTENTS_INDEX] = "Messages.contents";
m_fieldNames[MSG_DB_PDELETED_INDEX] = "Messages.is_pdeleted";
m_fieldNames[MSG_DB_ENCLOSURES_INDEX] = "Messages.enclosures";
m_fieldNames[MSG_DB_ACCOUNT_ID_INDEX] = "Messages.account_id";
m_fieldNames[MSG_DB_CUSTOM_ID_INDEX] = "Messages.custom_id";
m_fieldNames[MSG_DB_CUSTOM_HASH_INDEX] = "Messages.custom_hash";
m_fieldNames[MSG_DB_FEED_CUSTOM_ID_INDEX] = "Messages.feed";
// Set desired table and edit strategy.
// NOTE: Changes to the database are actually NOT submitted
// via model, but via DIRECT SQL calls are used to do persistent messages.
setTable(QSL("Messages"));
setEditStrategy(QSqlTableModel::OnManualSubmit);
loadMessages(nullptr);
}
@ -66,87 +45,45 @@ MessagesModel::~MessagesModel() {
qDebug("Destroying MessagesModel instance.");
}
QString MessagesModel::formatFields() const {
return m_fieldNames.values().join(QSL(", "));
}
QString MessagesModel::selectStatement() const {
return QL1S("SELECT ") + formatFields() +
QSL(" FROM Messages LEFT JOIN Feeds ON Messages.feed = Feeds.custom_id WHERE ") +
filter() + orderByClause() + QL1C(';');
}
QString MessagesModel::orderByClause() const {
if (m_sortColumn.isEmpty()) {
return QString();
}
else {
QStringList sorts;
for (int i = 0; i < m_sortColumn.size(); i++) {
QString field_name(m_fieldNames[m_sortColumn[i]]);
sorts.append(field_name + (m_sortOrder[i] == Qt::AscendingOrder ? QSL(" ASC") : QSL(" DESC")));
}
return QL1S(" ORDER BY ") + sorts.join(QSL(", "));
}
}
void MessagesModel::setupIcons() {
m_favoriteIcon = qApp->icons()->fromTheme(QSL("mail-mark-important"));
m_readIcon = qApp->icons()->fromTheme(QSL("mail-mark-read"));
m_unreadIcon = qApp->icons()->fromTheme(QSL("mail-mark-unread"));
}
void MessagesModel::fetchAllData() {
select();
void MessagesModel::repopulate() {
m_cache->clear();
setQuery(selectStatement(), m_db);
while (canFetchMore()) {
fetchMore();
}
}
void MessagesModel::addSortState(int column, Qt::SortOrder order) {
int existing = m_sortColumn.indexOf(column);
bool is_ctrl_pressed = (QApplication::queryKeyboardModifiers() & Qt::ControlModifier) == Qt::ControlModifier;
bool MessagesModel::setData(const QModelIndex &index, const QVariant &value, int role) {
Q_UNUSED(role)
if (existing >= 0) {
m_sortColumn.removeAt(existing);
m_sortOrder.removeAt(existing);
}
if (m_sortColumn.size() > MAX_MULTICOLUMN_SORT_STATES) {
// We support only limited number of sort states
// due to DB performance.
m_sortColumn.removeAt(0);
m_sortOrder.removeAt(0);
}
if (is_ctrl_pressed) {
// User is activating the multicolumn sort mode.
m_sortColumn.append(column);
m_sortOrder.append(order);
}
else {
m_sortColumn.prepend(column);
m_sortOrder.prepend(order);
}
qDebug("Added sort state, select statement is now:\n'%s'", qPrintable(selectStatement()));
m_cache->setData(index, value, record(index.row()));
return true;
}
void MessagesModel::setupFonts() {
m_normalFont = Application::font("MessagesView");
m_boldFont = m_normalFont;
m_boldFont.setBold(true);
m_normalStrikedFont = m_normalFont;
m_boldStrikedFont = m_boldFont;
m_normalStrikedFont.setStrikeOut(true);
m_boldStrikedFont.setStrikeOut(true);
}
void MessagesModel::loadMessages(RootItem *item) {
m_selectedItem = item;
if (item == nullptr) {
setFilter("0 > 1");
setFilter(QSL(DEFAULT_SQL_MESSAGES_FILTER));
}
else {
if (!item->getParentServiceRoot()->loadMessagesForItem(item, this)) {
@ -160,7 +97,7 @@ void MessagesModel::loadMessages(RootItem *item) {
}
}
fetchAllData();
repopulate();
}
bool MessagesModel::setMessageImportantById(int id, RootItem::Importance important) {
@ -181,11 +118,6 @@ bool MessagesModel::setMessageImportantById(int id, RootItem::Importance importa
return false;
}
bool MessagesModel::submitAll() {
qFatal("Submitting changes via model is not allowed.");
return false;
}
void MessagesModel::highlightMessages(MessagesModel::MessageHighlighter highlight) {
m_messageHighlighter = highlight;
emit layoutAboutToBeChanged();
@ -218,8 +150,8 @@ void MessagesModel::reloadWholeLayout() {
emit layoutChanged();
}
Message MessagesModel::messageAt(int row_index) const {
return Message::fromSqlRecord(record(row_index));
Message MessagesModel::messageAt(int row_index) const {
return Message::fromSqlRecord(m_cache->containsData(row_index) ? m_cache->record(row_index) : record(row_index));
}
void MessagesModel::setupHeaderData() {
@ -261,27 +193,29 @@ QVariant MessagesModel::data(int row, int column, int role) const {
}
QVariant MessagesModel::data(const QModelIndex &idx, int role) const {
// This message is not in cache, return real data from live query.
switch (role) {
// Human readable data for viewing.
case Qt::DisplayRole: {
int index_column = idx.column();
if (index_column == MSG_DB_DCREATED_INDEX) {
QDateTime dt = TextFactory::parseDateTime(QSqlQueryModel::data(idx, role).value<qint64>()).toLocalTime();
if (m_customDateFormat.isEmpty()) {
return TextFactory::parseDateTime(QSqlTableModel::data(idx,
role).value<qint64>()).toLocalTime().toString(Qt::DefaultLocaleShortDate);
return dt.toString(Qt::DefaultLocaleShortDate);
}
else {
return TextFactory::parseDateTime(QSqlTableModel::data(idx, role).value<qint64>()).toLocalTime().toString(m_customDateFormat);
return dt.toString(m_customDateFormat);
}
}
else if (index_column == MSG_DB_AUTHOR_INDEX) {
const QString author_name = QSqlTableModel::data(idx, role).toString();
const QString author_name = QSqlQueryModel::data(idx, role).toString();
return author_name.isEmpty() ? QSL("-") : author_name;
}
else if (index_column != MSG_DB_IMPORTANT_INDEX && index_column != MSG_DB_READ_INDEX) {
return QSqlTableModel::data(idx, role);
return QSqlQueryModel::data(idx, role);
}
else {
return QVariant();
@ -289,18 +223,41 @@ QVariant MessagesModel::data(const QModelIndex &idx, int role) const {
}
case Qt::EditRole:
return QSqlTableModel::data(idx, role);
return m_cache->containsData(idx.row()) ? m_cache->data(idx) : QSqlQueryModel::data(idx, role);
case Qt::FontRole:
return QSqlTableModel::data(index(idx.row(), MSG_DB_READ_INDEX)).toInt() == 1 ? m_normalFont : m_boldFont;
case Qt::FontRole: {
QModelIndex idx_read = index(idx.row(), MSG_DB_READ_INDEX);
QVariant data_read = m_cache->containsData(idx_read .row()) ? m_cache->data(idx_read ) : QSqlQueryModel::data(idx_read );
QModelIndex idx_del = index(idx.row(), MSG_DB_DELETED_INDEX);
QVariant data_del = m_cache->containsData(idx_del.row()) ? m_cache->data(idx_del) : QSqlQueryModel::data(idx_del);
const bool is_bin = qobject_cast<RecycleBin*>(loadedItem());
const bool is_deleted = data_del.toBool();
const bool striked = is_bin ^ is_deleted;
if (data_read.toBool()) {
return striked ? m_normalStrikedFont : m_normalFont;
}
else {
return striked ? m_boldStrikedFont : m_boldFont;
}
}
case Qt::ForegroundRole:
switch (m_messageHighlighter) {
case HighlightImportant:
return QSqlTableModel::data(index(idx.row(), MSG_DB_IMPORTANT_INDEX)).toInt() == 1 ? QColor(Qt::blue) : QVariant();
case HighlightImportant: {
QModelIndex idx_important = index(idx.row(), MSG_DB_IMPORTANT_INDEX);
QVariant dta = m_cache->containsData(idx_important.row()) ? m_cache->data(idx_important) : QSqlQueryModel::data(idx_important);
case HighlightUnread:
return QSqlTableModel::data(index(idx.row(), MSG_DB_READ_INDEX)).toInt() == 0 ? QColor(Qt::blue) : QVariant();
return dta.toInt() == 1 ? QColor(Qt::blue) : QVariant();
}
case HighlightUnread: {
QModelIndex idx_read = index(idx.row(), MSG_DB_READ_INDEX);
QVariant dta = m_cache->containsData(idx_read.row()) ? m_cache->data(idx_read) : QSqlQueryModel::data(idx_read);
return dta.toInt() == 0 ? QColor(Qt::blue) : QVariant();
}
case NoHighlighting:
default:
@ -311,10 +268,16 @@ QVariant MessagesModel::data(const QModelIndex &idx, int role) const {
const int index_column = idx.column();
if (index_column == MSG_DB_READ_INDEX) {
return QSqlTableModel::data(idx).toInt() == 1 ? m_readIcon : m_unreadIcon;
QModelIndex idx_read = index(idx.row(), MSG_DB_READ_INDEX);
QVariant dta = m_cache->containsData(idx_read.row()) ? m_cache->data(idx_read) : QSqlQueryModel::data(idx_read);
return dta.toInt() == 1 ? m_readIcon : m_unreadIcon;
}
else if (index_column == MSG_DB_IMPORTANT_INDEX) {
return QSqlTableModel::data(idx).toInt() == 1 ? m_favoriteIcon : QVariant();
QModelIndex idx_important = index(idx.row(), MSG_DB_IMPORTANT_INDEX);
QVariant dta = m_cache->containsData(idx_important.row()) ? m_cache->data(idx_important) : QSqlQueryModel::data(idx_important);
return dta.toInt() == 1 ? m_favoriteIcon : QVariant();
}
else {
return QVariant();
@ -349,7 +312,7 @@ bool MessagesModel::setMessageRead(int row_index, RootItem::ReadStatus read) {
return false;
}
if (DatabaseQueries::markMessagesReadUnread(database(), QStringList() << QString::number(message.m_id), read)) {
if (DatabaseQueries::markMessagesReadUnread(m_db, QStringList() << QString::number(message.m_id), read)) {
return m_selectedItem->getParentServiceRoot()->onAfterSetMessagesRead(m_selectedItem, QList<Message>() << message, read);
}
else {
@ -398,7 +361,7 @@ bool MessagesModel::switchMessageImportance(int row_index) {
}
// Commit changes.
if (DatabaseQueries::markMessageImportant(database(), message.m_id, next_importance)) {
if (DatabaseQueries::markMessageImportant(m_db, message.m_id, next_importance)) {
emit dataChanged(index(row_index, 0), index(row_index, MSG_DB_FEED_CUSTOM_ID_INDEX), QVector<int>() << Qt::FontRole);
return m_selectedItem->getParentServiceRoot()->onAfterSwitchMessageImportance(m_selectedItem,
@ -422,14 +385,20 @@ bool MessagesModel::switchBatchMessageImportance(const QModelIndexList &messages
RootItem::NotImportant :
RootItem::Important));
message_ids.append(QString::number(msg.m_id));
QModelIndex idx_msg_imp = index(message.row(), MSG_DB_IMPORTANT_INDEX);
setData(idx_msg_imp, message_importance == RootItem::Important ?
(int) RootItem::NotImportant :
(int) RootItem::Important);
}
reloadWholeLayout();
if (!m_selectedItem->getParentServiceRoot()->onBeforeSwitchMessageImportance(m_selectedItem, message_states)) {
return false;
}
if (DatabaseQueries::switchMessagesImportance(database(), message_ids)) {
fetchAllData();
if (DatabaseQueries::switchMessagesImportance(m_db, message_ids)) {
return m_selectedItem->getParentServiceRoot()->onAfterSwitchMessageImportance(m_selectedItem, message_states);
}
else {
@ -447,8 +416,12 @@ bool MessagesModel::setBatchMessagesDeleted(const QModelIndexList &messages) {
msgs.append(msg);
message_ids.append(QString::number(msg.m_id));
setData(index(message.row(), MSG_DB_DELETED_INDEX), 1);
}
reloadWholeLayout();
if (!m_selectedItem->getParentServiceRoot()->onBeforeMessagesDelete(m_selectedItem, msgs)) {
return false;
}
@ -456,14 +429,13 @@ bool MessagesModel::setBatchMessagesDeleted(const QModelIndexList &messages) {
bool deleted;
if (m_selectedItem->kind() != RootItemKind::Bin) {
deleted = DatabaseQueries::deleteOrRestoreMessagesToFromBin(database(), message_ids, true);
deleted = DatabaseQueries::deleteOrRestoreMessagesToFromBin(m_db, message_ids, true);
}
else {
deleted = DatabaseQueries::permanentlyDeleteMessages(database(), message_ids);
deleted = DatabaseQueries::permanentlyDeleteMessages(m_db, message_ids);
}
if (deleted) {
fetchAllData();
return m_selectedItem->getParentServiceRoot()->onAfterMessagesDelete(m_selectedItem, msgs);
}
else {
@ -481,14 +453,17 @@ bool MessagesModel::setBatchMessagesRead(const QModelIndexList &messages, RootIt
msgs.append(msg);
message_ids.append(QString::number(msg.m_id));
setData(index(message.row(), MSG_DB_READ_INDEX), (int) read);
}
reloadWholeLayout();
if (!m_selectedItem->getParentServiceRoot()->onBeforeSetMessagesRead(m_selectedItem, msgs, read)) {
return false;
}
if (DatabaseQueries::markMessagesReadUnread(database(), message_ids, read)) {
fetchAllData();
if (DatabaseQueries::markMessagesReadUnread(m_db, message_ids, read)) {
return m_selectedItem->getParentServiceRoot()->onAfterSetMessagesRead(m_selectedItem, msgs, read);
}
else {
@ -506,14 +481,17 @@ bool MessagesModel::setBatchMessagesRestored(const QModelIndexList &messages) {
msgs.append(msg);
message_ids.append(QString::number(msg.m_id));
setData(index(message.row(), MSG_DB_DELETED_INDEX), 0);
}
reloadWholeLayout();
if (!m_selectedItem->getParentServiceRoot()->onBeforeMessagesRestoredFromBin(m_selectedItem, msgs)) {
return false;
}
if (DatabaseQueries::deleteOrRestoreMessagesToFromBin(database(), message_ids, false)) {
fetchAllData();
if (DatabaseQueries::deleteOrRestoreMessagesToFromBin(m_db, message_ids, false)) {
return m_selectedItem->getParentServiceRoot()->onAfterMessagesRestoredFromBin(m_selectedItem, msgs);
}
else {

View File

@ -18,7 +18,8 @@
#ifndef MESSAGESMODEL_H
#define MESSAGESMODEL_H
#include <QSqlTableModel>
#include <QSqlQueryModel>
#include "core/messagesmodelsqllayer.h"
#include "definitions/definitions.h"
#include "core/message.h"
@ -28,7 +29,9 @@
#include <QIcon>
class MessagesModel : public QSqlTableModel {
class MessagesModelCache;
class MessagesModel : public QSqlQueryModel, public MessagesModelSqlLayer {
Q_OBJECT
public:
@ -44,7 +47,12 @@ class MessagesModel : public QSqlTableModel {
explicit MessagesModel(QObject *parent = 0);
virtual ~MessagesModel();
// Fetches ALL available data to the model.
// NOTE: This activates the SQL query and populates the model with new data.
void repopulate();
// Model implementation.
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
QVariant data(int row, int column, int role = Qt::DisplayRole) const;
QVariant headerData(int section, Qt::Orientation orientation, int role) const;
@ -59,27 +67,17 @@ class MessagesModel : public QSqlTableModel {
void updateDateFormat();
void reloadWholeLayout();
// Adds this new state to queue of sort states.
void addSortState(int column, Qt::SortOrder order);
// CORE messages manipulators.
// NOTE: These are used to change properties of one message.
// NOTE: Model is NOT reset after one of these methods are applied
// but changes ARE written to the database.
// SINGLE message manipulators.
bool switchMessageImportance(int row_index);
bool setMessageRead(int row_index, RootItem::ReadStatus read);
// BATCH messages manipulators.
// NOTE: These methods are used for changing of attributes of
// many messages via DIRECT SQL calls.
// NOTE: Model is reset after one of these methods is applied and
// changes ARE written to the database.
bool switchBatchMessageImportance(const QModelIndexList &messages);
bool setBatchMessagesDeleted(const QModelIndexList &messages);
bool setBatchMessagesRead(const QModelIndexList &messages, RootItem::ReadStatus read);
bool setBatchMessagesRestored(const QModelIndexList &messages);
// Filters messages
// Highlights messages.
void highlightMessages(MessageHighlighter highlight);
// Loads messages of given feeds.
@ -91,30 +89,12 @@ class MessagesModel : public QSqlTableModel {
bool setMessageImportantById(int id, RootItem::Importance important);
bool setMessageReadById(int id, RootItem::ReadStatus read);
private slots:
// To disable persistent changes submissions.
bool submitAll();
private:
void setupHeaderData();
void setupFonts();
void setupIcons();
// Fetches ALL available data to the model.
void fetchAllData();
// Direct SQL stuff.
QString orderByClause() const;
QString selectStatement() const;
QString formatFields() const;
// NOTE: These two lists contain data for multicolumn sorting.
// They are always same length. Most important sort column/order
// are located at the start of lists;
QMap<int,QString> m_fieldNames;
QList<int> m_sortColumn;
QList<Qt::SortOrder> m_sortOrder;
MessagesModelCache *m_cache;
MessageHighlighter m_messageHighlighter;
QString m_customDateFormat;
@ -124,6 +104,8 @@ class MessagesModel : public QSqlTableModel {
QFont m_normalFont;
QFont m_boldFont;
QFont m_normalStrikedFont;
QFont m_boldStrikedFont;
QIcon m_favoriteIcon;
QIcon m_readIcon;

39
src/core/messagesmodelcache.cpp Executable file
View File

@ -0,0 +1,39 @@
// This file is part of RSS Guard.
//
// Copyright (C) 2011-2017 by Martin Rotter <rotter.martinos@gmail.com>
//
// RSS Guard is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// RSS Guard is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with RSS Guard. If not, see <http://www.gnu.org/licenses/>.
#include "core/messagesmodelcache.h"
#include "miscellaneous/textfactory.h"
MessagesModelCache::MessagesModelCache(QObject *parent) : QObject(parent), m_msgCache(QHash<int,QSqlRecord>()) {
}
MessagesModelCache::~MessagesModelCache() {
}
void MessagesModelCache::setData(const QModelIndex &index, const QVariant &value, QSqlRecord &record) {
if (!m_msgCache.contains(index.row())) {
m_msgCache[index.row()] = record;
}
m_msgCache[index.row()].setValue(index.column(), value);
}
QVariant MessagesModelCache::data(const QModelIndex &idx) {
return m_msgCache[idx.row()].value(idx.column());
}

55
src/core/messagesmodelcache.h Executable file
View File

@ -0,0 +1,55 @@
// This file is part of RSS Guard.
//
// Copyright (C) 2011-2017 by Martin Rotter <rotter.martinos@gmail.com>
//
// RSS Guard is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// RSS Guard is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with RSS Guard. If not, see <http://www.gnu.org/licenses/>.
#ifndef MESSAGESMODELCACHE_H
#define MESSAGESMODELCACHE_H
#include <QObject>
#include "core/message.h"
#include <QVariant>
#include <QModelIndex>
class MessagesModelCache : public QObject {
Q_OBJECT
public:
explicit MessagesModelCache(QObject *parent = nullptr);
virtual ~MessagesModelCache();
inline bool containsData(int row_idx) const {
return m_msgCache.contains(row_idx);
}
inline QSqlRecord record(int row_idx) const {
return m_msgCache.value(row_idx);
}
inline void clear() {
m_msgCache.clear();
}
void setData(const QModelIndex &index, const QVariant &value, QSqlRecord &record);
QVariant data(const QModelIndex &idx);
private:
QHash<int,QSqlRecord> m_msgCache;
};
#endif // MESSAGESMODELCACHE_H

View File

@ -0,0 +1,105 @@
// This file is part of RSS Guard.
//
// Copyright (C) 2011-2017 by Martin Rotter <rotter.martinos@gmail.com>
//
// RSS Guard is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// RSS Guard is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with RSS Guard. If not, see <http://www.gnu.org/licenses/>.
#include "core/messagesmodelsqllayer.h"
#include "definitions/definitions.h"
#include "miscellaneous/application.h"
MessagesModelSqlLayer::MessagesModelSqlLayer()
: m_filter(QSL(DEFAULT_SQL_MESSAGES_FILTER)), m_fieldNames(QMap<int,QString>()),
m_sortColumns(QList<int>()), m_sortOrders(QList<Qt::SortOrder>()){
m_db = qApp->database()->connection(QSL("MessagesModel"), DatabaseFactory::FromSettings);
m_fieldNames[MSG_DB_ID_INDEX] = "Messages.id";
m_fieldNames[MSG_DB_READ_INDEX] = "Messages.is_read";
m_fieldNames[MSG_DB_DELETED_INDEX] = "Messages.is_deleted";
m_fieldNames[MSG_DB_IMPORTANT_INDEX] = "Messages.is_important";
m_fieldNames[MSG_DB_FEED_TITLE_INDEX] = "Feeds.title";
m_fieldNames[MSG_DB_TITLE_INDEX] = "Messages.title";
m_fieldNames[MSG_DB_URL_INDEX] = "Messages.url";
m_fieldNames[MSG_DB_AUTHOR_INDEX] = "Messages.author";
m_fieldNames[MSG_DB_DCREATED_INDEX] = "Messages.date_created";
m_fieldNames[MSG_DB_CONTENTS_INDEX] = "Messages.contents";
m_fieldNames[MSG_DB_PDELETED_INDEX] = "Messages.is_pdeleted";
m_fieldNames[MSG_DB_ENCLOSURES_INDEX] = "Messages.enclosures";
m_fieldNames[MSG_DB_ACCOUNT_ID_INDEX] = "Messages.account_id";
m_fieldNames[MSG_DB_CUSTOM_ID_INDEX] = "Messages.custom_id";
m_fieldNames[MSG_DB_CUSTOM_HASH_INDEX] = "Messages.custom_hash";
m_fieldNames[MSG_DB_FEED_CUSTOM_ID_INDEX] = "Messages.feed";
}
void MessagesModelSqlLayer::addSortState(int column, Qt::SortOrder order) {
int existing = m_sortColumns.indexOf(column);
bool is_ctrl_pressed = (QApplication::queryKeyboardModifiers() & Qt::ControlModifier) == Qt::ControlModifier;
if (existing >= 0) {
m_sortColumns.removeAt(existing);
m_sortOrders.removeAt(existing);
}
if (m_sortColumns.size() > MAX_MULTICOLUMN_SORT_STATES) {
// We support only limited number of sort states
// due to DB performance.
m_sortColumns.removeAt(0);
m_sortOrders.removeAt(0);
}
if (is_ctrl_pressed) {
// User is activating the multicolumn sort mode.
m_sortColumns.append(column);
m_sortOrders.append(order);
}
else {
m_sortColumns.prepend(column);
m_sortOrders.prepend(order);
}
qDebug("Added sort state, select statement is now:\n'%s'", qPrintable(selectStatement()));
}
void MessagesModelSqlLayer::setFilter(const QString &filter) {
m_filter = filter;
}
QString MessagesModelSqlLayer::formatFields() const {
return m_fieldNames.values().join(QSL(", "));
}
QString MessagesModelSqlLayer::selectStatement() const {
return QL1S("SELECT ") + formatFields() +
QSL(" FROM Messages LEFT JOIN Feeds ON Messages.feed = Feeds.custom_id WHERE ") +
m_filter + orderByClause() + QL1C(';');
}
QString MessagesModelSqlLayer::orderByClause() const {
if (m_sortColumns.isEmpty()) {
return QString();
}
else {
QStringList sorts;
for (int i = 0; i < m_sortColumns.size(); i++) {
QString field_name(m_fieldNames[m_sortColumns[i]]);
sorts.append(field_name + (m_sortOrders[i] == Qt::AscendingOrder ? QSL(" ASC") : QSL(" DESC")));
}
return QL1S(" ORDER BY ") + sorts.join(QSL(", "));
}
}

View File

@ -0,0 +1,56 @@
// This file is part of RSS Guard.
//
// Copyright (C) 2011-2017 by Martin Rotter <rotter.martinos@gmail.com>
//
// RSS Guard is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// RSS Guard is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with RSS Guard. If not, see <http://www.gnu.org/licenses/>.
#ifndef MESSAGESMODELSQLLAYER_H
#define MESSAGESMODELSQLLAYER_H
#include <QSqlDatabase>
#include <QMap>
#include <QList>
class MessagesModelSqlLayer {
public:
explicit MessagesModelSqlLayer();
// Adds this new state to queue of sort states.
void addSortState(int column, Qt::SortOrder order);
// Sets SQL WHERE clause, without "WHERE" keyword.
void setFilter(const QString &filter);
protected:
QString orderByClause() const;
QString selectStatement() const;
QString formatFields() const;
QSqlDatabase m_db;
private:
QString m_filter;
// NOTE: These two lists contain data for multicolumn sorting.
// They are always same length. Most important sort column/order
// are located at the start of lists;
QMap<int,QString> m_fieldNames;
QList<int> m_sortColumns;
QList<Qt::SortOrder> m_sortOrders;
};
#endif // MESSAGESMODELSQLLAYER_H

View File

@ -26,6 +26,7 @@
#define ARGUMENTS_LIST_SEPARATOR "\n"
#define DEFAULT_SQL_MESSAGES_FILTER "0 > 1"
#define MAX_MULTICOLUMN_SORT_STATES 3
#define ENCLOSURES_OUTER_SEPARATOR '#'
#define ECNLOSURES_INNER_SEPARATOR '&'

View File

@ -114,7 +114,7 @@ void FormMain::showDbCleanupAssistant() {
qApp->feedUpdateLock()->unlock();
tabWidget()->feedMessageViewer()->messagesView()->reloadSelections(false);
tabWidget()->feedMessageViewer()->messagesView()->reloadSelections();
qApp->feedReader()->feedsModel()->reloadCountsOfWholeModel();
}
else {
@ -353,7 +353,7 @@ void FormMain::onFeedUpdatesFinished(const FeedDownloadResults &results) {
Q_UNUSED(results)
statusBar()->clearProgressFeeds();
tabWidget()->feedMessageViewer()->messagesView()->reloadSelections(false);
tabWidget()->feedMessageViewer()->messagesView()->reloadSelections();
}
void FormMain::onFeedUpdatesStarted() {

View File

@ -35,10 +35,7 @@
MessagesView::MessagesView(QWidget *parent)
: QTreeView(parent),
m_contextMenu(nullptr),
m_columnsAdjusted(false),
m_batchUnreadSwitch(false) {
: QTreeView(parent), m_contextMenu(nullptr), m_columnsAdjusted(false) {
m_sourceModel = qApp->feedReader()->messagesModel();
m_proxyModel = qApp->feedReader()->messagesProxyModel();
@ -60,10 +57,7 @@ void MessagesView::sort(int column, Qt::SortOrder order, bool repopulate_data, b
m_sourceModel->addSortState(column, order);
if (repopulate_data) {
m_sourceModel->sort(column, order);
}
else {
m_sourceModel->setSort(column, order);
m_sourceModel->repopulate();
}
if (change_header) {
@ -88,7 +82,7 @@ void MessagesView::keyboardSearch(const QString &search) {
setSelectionMode(QAbstractItemView::ExtendedSelection);
}
void MessagesView::reloadSelections(bool mark_current_index_read) {
void MessagesView::reloadSelections() {
const QDateTime dt1 = QDateTime::currentDateTime();
QModelIndex current_index = selectionModel()->currentIndex();
@ -123,17 +117,9 @@ void MessagesView::reloadSelections(bool mark_current_index_read) {
}
if (current_index.isValid()) {
if (!mark_current_index_read) {
// User selected to mark some messages as unread, if one
// of them will be marked as current, then it will be read again.
m_batchUnreadSwitch = true;
}
scrollTo(current_index);
setCurrentIndex(current_index);
reselectIndexes(QModelIndexList() << current_index);
m_batchUnreadSwitch = false;
}
else {
// Messages were probably removed from the model, nothing can
@ -263,12 +249,10 @@ void MessagesView::selectionChanged(const QItemSelection &selected, const QItemS
if (mapped_current_index.isValid() && selected_rows.count() > 0) {
Message message = m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row());
if (!m_batchUnreadSwitch) {
// Set this message as read only if current item
// wasn't changed by "mark selected messages unread" action.
m_sourceModel->setMessageRead(mapped_current_index.row(), RootItem::Read);
message.m_isRead = true;
}
// Set this message as read only if current item
// wasn't changed by "mark selected messages unread" action.
m_sourceModel->setMessageRead(mapped_current_index.row(), RootItem::Read);
message.m_isRead = true;
emit currentMessageChanged(message, m_sourceModel->loadedItem());
}
@ -356,26 +340,14 @@ void MessagesView::setSelectedMessagesReadStatus(RootItem::ReadStatus read) {
return;
}
const QModelIndex mapped_current_index = m_proxyModel->mapToSource(current_index);
QModelIndexList selected_indexes = selectionModel()->selectedRows();
const QModelIndexList mapped_indexes = m_proxyModel->mapListToSource(selected_indexes);
m_sourceModel->setBatchMessagesRead(mapped_indexes, read);
selected_indexes = m_proxyModel->mapListFromSource(mapped_indexes, true);
current_index = m_proxyModel->mapFromSource(m_sourceModel->index(mapped_current_index.row(), mapped_current_index.column()));
current_index = m_proxyModel->index(current_index.row(), current_index.column());
if (current_index.isValid()) {
if (read == RootItem::Unread) {
// User selected to mark some messages as unread, if one
// of them will be marked as current, then it will be read again.
m_batchUnreadSwitch = true;
}
setCurrentIndex(current_index);
scrollTo(current_index);
reselectIndexes(selected_indexes);
m_batchUnreadSwitch = false;
emit currentMessageChanged(m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row()), m_sourceModel->loadedItem());
}
else {
emit currentMessageRemoved();
@ -383,7 +355,7 @@ void MessagesView::setSelectedMessagesReadStatus(RootItem::ReadStatus read) {
}
void MessagesView::deleteSelectedMessages() {
const QModelIndex current_index = selectionModel()->currentIndex();
QModelIndex current_index = selectionModel()->currentIndex();
if (!current_index.isValid()) {
return;
@ -393,17 +365,10 @@ void MessagesView::deleteSelectedMessages() {
const QModelIndexList mapped_indexes = m_proxyModel->mapListToSource(selected_indexes);
m_sourceModel->setBatchMessagesDeleted(mapped_indexes);
current_index = m_proxyModel->index(current_index.row(), current_index.column());
const int row_count = m_proxyModel->rowCount();
if (row_count > 0) {
const QModelIndex last_item = current_index.row() < row_count ?
m_proxyModel->index(current_index.row(), MSG_DB_TITLE_INDEX) :
m_proxyModel->index(row_count - 1, MSG_DB_TITLE_INDEX);
setCurrentIndex(last_item);
scrollTo(last_item);
reselectIndexes(QModelIndexList() << last_item);
if (current_index.isValid()) {
emit currentMessageChanged(m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row()), m_sourceModel->loadedItem());
}
else {
emit currentMessageRemoved();
@ -411,7 +376,7 @@ void MessagesView::deleteSelectedMessages() {
}
void MessagesView::restoreSelectedMessages() {
const QModelIndex current_index = selectionModel()->currentIndex();
QModelIndex current_index = selectionModel()->currentIndex();
if (!current_index.isValid()) {
return;
@ -421,17 +386,11 @@ void MessagesView::restoreSelectedMessages() {
const QModelIndexList mapped_indexes = m_proxyModel->mapListToSource(selected_indexes);
m_sourceModel->setBatchMessagesRestored(mapped_indexes);
current_index = m_proxyModel->index(current_index.row(), current_index.column());
const int row_count = m_sourceModel->rowCount();
if (current_index.isValid()) {
emit currentMessageChanged(m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row()), m_sourceModel->loadedItem());
if (row_count > 0) {
const QModelIndex last_item = current_index.row() < row_count ?
m_proxyModel->index(current_index.row(), MSG_DB_TITLE_INDEX) :
m_proxyModel->index(row_count - 1, MSG_DB_TITLE_INDEX);
setCurrentIndex(last_item);
scrollTo(last_item);
reselectIndexes(QModelIndexList() << last_item);
}
else {
emit currentMessageRemoved();
@ -445,22 +404,14 @@ void MessagesView::switchSelectedMessagesImportance() {
return;
}
const QModelIndex mapped_current_index = m_proxyModel->mapToSource(current_index);
QModelIndexList selected_indexes = selectionModel()->selectedRows();
const QModelIndexList mapped_indexes = m_proxyModel->mapListToSource(selected_indexes);
m_sourceModel->switchBatchMessageImportance(mapped_indexes);
selected_indexes = m_proxyModel->mapListFromSource(mapped_indexes, true);
current_index = m_proxyModel->mapFromSource(m_sourceModel->index(mapped_current_index.row(),
mapped_current_index.column()));
current_index = m_proxyModel->index(current_index.row(), current_index.column());
if (current_index.isValid()) {
m_batchUnreadSwitch = true;
scrollTo(current_index);
setCurrentIndex(current_index);
reselectIndexes(QModelIndexList() << selected_indexes);
m_batchUnreadSwitch = false;
emit currentMessageChanged(m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row()), m_sourceModel->loadedItem());
}
else {
// Messages were probably removed from the model, nothing can

View File

@ -50,9 +50,7 @@ class MessagesView : public QTreeView {
// Called after data got changed externally
// and it needs to be reloaded to the view.
// If "mark_current_index_read" is 0, then message with
// "current" index is not marked as read.
void reloadSelections(bool mark_current_index_read);
void reloadSelections();
// Loads un-deleted messages from selected feeds.
void loadItem(RootItem *item);
@ -122,7 +120,6 @@ class MessagesView : public QTreeView {
MessagesModel *m_sourceModel;
bool m_columnsAdjusted;
bool m_batchUnreadSwitch;
};
#endif // MESSAGESVIEW_H

View File

@ -126,19 +126,6 @@ void WebBrowser::loadUrl(const QString &url) {
}
void WebBrowser::loadMessages(const QList<Message> &messages, RootItem *root) {
if (m_messages.size() == messages.size()) {
for (int i = 0; i < messages.size(); i++) {
if (m_messages.at(i).m_id != messages.at(i).m_id) {
break;
}
if (i == messages.size() - 1) {
// We checked last items, both collections contain the same messages.
return;
}
}
}
m_messages = messages;
m_root = root;

View File

@ -18,6 +18,7 @@
#include "services/abstract/serviceroot.h"
#include "core/feedsmodel.h"
#include "core/messagesmodel.h"
#include "miscellaneous/application.h"
#include "miscellaneous/iconfactory.h"
#include "miscellaneous/textfactory.h"
@ -26,8 +27,6 @@
#include "services/abstract/feed.h"
#include "services/abstract/recyclebin.h"
#include <QSqlTableModel>
ServiceRoot::ServiceRoot(RootItem *parent) : RootItem(parent), m_accountId(NO_PARENT_CATEGORY) {
setKind(RootItemKind::ServiceRoot);
@ -386,7 +385,7 @@ void ServiceRoot::setAccountId(int account_id) {
m_accountId = account_id;
}
bool ServiceRoot::loadMessagesForItem(RootItem *item, QSqlTableModel *model) {
bool ServiceRoot::loadMessagesForItem(RootItem *item, MessagesModel *model) {
if (item->kind() == RootItemKind::Bin) {
model->setFilter(QString("Messages.is_deleted = 1 AND Messages.is_pdeleted = 0 AND Messages.account_id = %1").arg(QString::number(accountId())));
}

View File

@ -28,7 +28,7 @@
class FeedsModel;
class RecycleBin;
class QAction;
class QSqlTableModel;
class MessagesModel;
// Car here represents ID of the item.
typedef QList<QPair<int,RootItem*> > Assignment;
@ -102,7 +102,7 @@ class ServiceRoot : public RootItem {
// and then use method QSqlTableModel::setFilter(....).
// NOTE: It would be more preferable if all messages are downloaded
// right when feeds are updated.
virtual bool loadMessagesForItem(RootItem *item, QSqlTableModel *model);
virtual bool loadMessagesForItem(RootItem *item, MessagesModel *model);
// Called BEFORE this read status update (triggered by user in message list) is stored in DB,
// when false is returned, change is aborted.