From 67e9a249afa8863464dd0e07d2f9b64f5c32be9e Mon Sep 17 00:00:00 2001 From: Martin Rotter Date: Mon, 14 Mar 2022 09:24:30 +0100 Subject: [PATCH] Revert "cleanup manual sort partial implementation" This reverts commit 741c6ef68281460035b03f7b9b695d90045b28d6. --- resources/sql/db_init_sqlite.sql | 3 + resources/sql/db_update_sqlite_1_2.sql | 60 +++++++- src/librssguard/core/feedsmodel.cpp | 6 + src/librssguard/core/feedsmodel.h | 2 + src/librssguard/core/feedsproxymodel.cpp | 40 ++++- src/librssguard/core/feedsproxymodel.h | 3 + src/librssguard/database/databasequeries.cpp | 145 ++++++++++++++++-- src/librssguard/database/databasequeries.h | 7 + src/librssguard/definitions/definitions.h | 42 ++--- src/librssguard/gui/dialogs/formmain.cpp | 23 +++ src/librssguard/gui/dialogs/formmain.ui | 43 ++++++ src/librssguard/gui/feedsview.cpp | 18 +++ src/librssguard/gui/feedsview.h | 4 + src/librssguard/miscellaneous/settings.cpp | 3 + src/librssguard/miscellaneous/settings.h | 3 + .../services/abstract/rootitem.cpp | 11 +- src/librssguard/services/abstract/rootitem.h | 15 ++ 17 files changed, 389 insertions(+), 39 deletions(-) diff --git a/resources/sql/db_init_sqlite.sql b/resources/sql/db_init_sqlite.sql index 5fd1b067b..867eb8ab8 100644 --- a/resources/sql/db_init_sqlite.sql +++ b/resources/sql/db_init_sqlite.sql @@ -5,6 +5,7 @@ CREATE TABLE Information ( -- ! CREATE TABLE Accounts ( id $$, + ordr INTEGER NOT NULL CHECK (ordr >= 0), type TEXT NOT NULL CHECK (type != ''), /* ID of the account type. Each account defines its own, for example 'ttrss'. */ proxy_type INTEGER NOT NULL DEFAULT 0 CHECK (proxy_type >= 0), proxy_host TEXT, @@ -17,6 +18,7 @@ CREATE TABLE Accounts ( -- ! CREATE TABLE Categories ( id $$, + ordr INTEGER NOT NULL CHECK (ordr >= 0), parent_id INTEGER NOT NULL CHECK (parent_id >= -1), /* Root categories contain -1 here. */ title TEXT NOT NULL CHECK (title != ''), description TEXT, @@ -30,6 +32,7 @@ CREATE TABLE Categories ( -- ! CREATE TABLE Feeds ( id $$, + ordr INTEGER NOT NULL CHECK (ordr >= 0), title TEXT NOT NULL CHECK (title != ''), description TEXT, date_created BIGINT, diff --git a/resources/sql/db_update_sqlite_1_2.sql b/resources/sql/db_update_sqlite_1_2.sql index d4882b63b..f44563c70 100644 --- a/resources/sql/db_update_sqlite_1_2.sql +++ b/resources/sql/db_update_sqlite_1_2.sql @@ -2,6 +2,7 @@ ALTER TABLE Feeds RENAME TO backup_Feeds; -- ! CREATE TABLE Feeds ( id $$, + ordr INTEGER NOT NULL CHECK (ordr >= 0), title TEXT NOT NULL CHECK (title != ''), description TEXT, date_created BIGINT, @@ -24,4 +25,61 @@ INSERT INTO Feeds (id, title, description, date_created, icon, category, source, SELECT id, title, description, date_created, icon, category, source, update_type, update_interval, account_id, custom_id, custom_data FROM backup_Feeds; -- ! -DROP TABLE backup_Feeds; \ No newline at end of file +DROP TABLE backup_Feeds; +-- ! +UPDATE Feeds +SET ordr = ( + SELECT COUNT(*) + FROM Feeds ct + WHERE Feeds.account_id = ct.account_id AND Feeds.category = ct.category AND ct.id < Feeds.id +); +-- ! +ALTER TABLE Categories RENAME TO backup_Categories; +-- ! +CREATE TABLE Categories ( + id $$, + ordr INTEGER NOT NULL CHECK (ordr >= 0), + parent_id INTEGER NOT NULL CHECK (parent_id >= -1), /* Root categories contain -1 here. */ + title TEXT NOT NULL CHECK (title != ''), + description TEXT, + date_created BIGINT, + icon ^^, + account_id INTEGER NOT NULL, + custom_id TEXT, + + FOREIGN KEY (account_id) REFERENCES Accounts (id) ON DELETE CASCADE +); +-- ! +INSERT INTO Categories (id, ordr, parent_id, title, description, date_created, icon, account_id, custom_id) +SELECT id, id, parent_id, title, description, date_created, icon, account_id, custom_id +FROM backup_Categories; +-- ! +DROP TABLE backup_Categories; +-- ! +UPDATE Categories +SET ordr = ( + SELECT COUNT(*) + FROM Categories ct + WHERE Categories.account_id = ct.account_id AND Categories.parent_id = ct.parent_id AND ct.id < Categories.id +); +-- ! +ALTER TABLE Accounts RENAME TO backup_Accounts; +-- ! +CREATE TABLE Accounts ( + id $$, + ordr INTEGER NOT NULL CHECK (ordr >= 0), + type TEXT NOT NULL CHECK (type != ''), /* ID of the account type. Each account defines its own, for example 'ttrss'. */ + proxy_type INTEGER NOT NULL DEFAULT 0 CHECK (proxy_type >= 0), + proxy_host TEXT, + proxy_port INTEGER, + proxy_username TEXT, + proxy_password TEXT, + /* Custom column for (serialized) custom account-specific data. */ + custom_data TEXT +); +-- ! +INSERT INTO Accounts (id, ordr, type, proxy_type, proxy_host, proxy_port, proxy_username, proxy_password, custom_data) +SELECT id, id - 1, type, proxy_type, proxy_host, proxy_port, proxy_username, proxy_password, custom_data +FROM backup_Accounts; +-- ! +DROP TABLE backup_Accounts; \ No newline at end of file diff --git a/src/librssguard/core/feedsmodel.cpp b/src/librssguard/core/feedsmodel.cpp index 700c8e928..ea1f05632 100644 --- a/src/librssguard/core/feedsmodel.cpp +++ b/src/librssguard/core/feedsmodel.cpp @@ -524,6 +524,12 @@ bool FeedsModel::emptyAllBins() { return result; } +void FeedsModel::changeSortOrder(RootItem* item, bool move_top, bool move_bottom, int new_sort_order) { + QSqlDatabase db = qApp->database()->driver()->connection(metaObject()->className()); + + DatabaseQueries::moveItem(item, move_top, move_bottom, new_sort_order, db); +} + void FeedsModel::loadActivatedServiceAccounts() { auto serv = qApp->feedReader()->feedServices(); diff --git a/src/librssguard/core/feedsmodel.h b/src/librssguard/core/feedsmodel.h index 7a29d6839..0b55473c9 100644 --- a/src/librssguard/core/feedsmodel.h +++ b/src/librssguard/core/feedsmodel.h @@ -107,6 +107,8 @@ class RSSGUARD_DLLSPEC FeedsModel : public QAbstractItemModel { bool restoreAllBins(); bool emptyAllBins(); + void changeSortOrder(RootItem* item, bool move_top, bool move_bottom, int new_sort_order); + // Feeds operations. bool markItemRead(RootItem* item, RootItem::ReadStatus read); bool markItemCleared(RootItem* item, bool clean_read_only); diff --git a/src/librssguard/core/feedsproxymodel.cpp b/src/librssguard/core/feedsproxymodel.cpp index 720d6511e..2787dadb9 100644 --- a/src/librssguard/core/feedsproxymodel.cpp +++ b/src/librssguard/core/feedsproxymodel.cpp @@ -13,7 +13,7 @@ FeedsProxyModel::FeedsProxyModel(FeedsModel* source_model, QObject* parent) : QSortFilterProxyModel(parent), m_sourceModel(source_model), m_view(nullptr), - m_selectedItem(nullptr), m_showUnreadOnly(false) { + m_selectedItem(nullptr), m_showUnreadOnly(false), m_sortAlphabetically(true) { setObjectName(QSL("FeedsProxyModel")); setSortRole(Qt::ItemDataRole::EditRole); @@ -171,14 +171,32 @@ bool FeedsProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right return sortOrder() == Qt::SortOrder::DescendingOrder; } else if (left_item->kind() == right_item->kind()) { - // Both items are of the same type. - if (left.column() == FDS_MODEL_COUNTS_INDEX) { - // User wants to sort according to counts. - return left_item->countOfUnreadMessages() < right_item->countOfUnreadMessages(); + if (m_sortAlphabetically) { + // Both items are of the same type. + if (left.column() == FDS_MODEL_COUNTS_INDEX) { + // User wants to sort according to counts. + return left_item->countOfUnreadMessages() < right_item->countOfUnreadMessages(); + } + else { + // In other cases, sort by title. + return QString::localeAwareCompare(left_item->title().toLower(), right_item->title().toLower()) < 0; + } } else { - // In other cases, sort by title. - return QString::localeAwareCompare(left_item->title().toLower(), right_item->title().toLower()) < 0; + // We sort some types with sort order, other alphabetically. + switch (left_item->kind()) { + case RootItem::Kind::Feed: + case RootItem::Kind::Category: + case RootItem::Kind::ServiceRoot: + return sortOrder() == Qt::SortOrder::AscendingOrder + ? left_item->sortOrder() < right_item->sortOrder() + : left_item->sortOrder() > right_item->sortOrder(); + + default: + return sortOrder() == Qt::SortOrder::AscendingOrder + ? QString::localeAwareCompare(left_item->title().toLower(), right_item->title().toLower()) < 0 + : QString::localeAwareCompare(left_item->title().toLower(), right_item->title().toLower()) > 0; + } } } else { @@ -297,6 +315,14 @@ void FeedsProxyModel::setShowUnreadOnly(bool show_unread_only) { qApp->settings()->setValue(GROUP(Feeds), Feeds::ShowOnlyUnreadFeeds, show_unread_only); } +void FeedsProxyModel::setSortAlphabetically(bool sort_alphabetically) { + if (sort_alphabetically != m_sortAlphabetically) { + m_sortAlphabetically = sort_alphabetically; + qApp->settings()->setValue(GROUP(Feeds), Feeds::SortAlphabetically, sort_alphabetically); + invalidate(); + } +} + QModelIndexList FeedsProxyModel::mapListToSource(const QModelIndexList& indexes) const { QModelIndexList source_indexes; diff --git a/src/librssguard/core/feedsproxymodel.h b/src/librssguard/core/feedsproxymodel.h index f59485ff4..d78a46053 100644 --- a/src/librssguard/core/feedsproxymodel.h +++ b/src/librssguard/core/feedsproxymodel.h @@ -34,6 +34,8 @@ class FeedsProxyModel : public QSortFilterProxyModel { void setSelectedItem(const RootItem* selected_item); void setView(FeedsView* newView); + void setSortAlphabetically(bool sort_alphabetically); + public slots: void invalidateReadFeedsFilter(bool set_new_value = false, bool show_unread_only = false); @@ -52,6 +54,7 @@ class FeedsProxyModel : public QSortFilterProxyModel { FeedsView* m_view; const RootItem* m_selectedItem; bool m_showUnreadOnly; + bool m_sortAlphabetically; QList> m_hiddenIndices; QList m_priorities; }; diff --git a/src/librssguard/database/databasequeries.cpp b/src/librssguard/database/databasequeries.cpp index 73e96a98e..b4dd20383 100644 --- a/src/librssguard/database/databasequeries.cpp +++ b/src/librssguard/database/databasequeries.cpp @@ -1939,9 +1939,26 @@ void DatabaseQueries::createOverwriteCategory(const QSqlDatabase& db, Category* if (category->id() <= 0) { // We need to insert category first. + if (category->sortOrder() < 0) { + q.prepare(QSL("SELECT MAX(ordr) FROM Categories WHERE account_id = :account_id AND parent_id = :parent_id;")); + q.bindValue(QSL(":account_id"), account_id); + q.bindValue(QSL(":parent_id"), parent_id); + + if (!q.exec()) { + throw ApplicationException(q.lastError().text()); + } + + q.next(); + + int next_order = (q.value(0).isNull() ? -1 : q.value(0).toInt()) + 1; + + category->setSortOrder(next_order); + q.finish(); + } + q.prepare(QSL("INSERT INTO " - "Categories (parent_id, title, date_created, account_id) " - "VALUES (0, 'new', 0, %1);").arg(QString::number(account_id))); + "Categories (parent_id, ordr, title, date_created, account_id) " + "VALUES (0, 0, 'new', 0, %1);").arg(QString::number(account_id))); if (!q.exec()) { throw ApplicationException(q.lastError().text()); @@ -1952,7 +1969,7 @@ void DatabaseQueries::createOverwriteCategory(const QSqlDatabase& db, Category* } q.prepare("UPDATE Categories " - "SET parent_id = :parent_id, title = :title, description = :description, date_created = :date_created, " + "SET parent_id = :parent_id, ordr = :ordr, title = :title, description = :description, date_created = :date_created, " " icon = :icon, account_id = :account_id, custom_id = :custom_id " "WHERE id = :id;"); q.bindValue(QSL(":parent_id"), parent_id); @@ -1963,6 +1980,7 @@ void DatabaseQueries::createOverwriteCategory(const QSqlDatabase& db, Category* q.bindValue(QSL(":account_id"), account_id); q.bindValue(QSL(":custom_id"), category->customId()); q.bindValue(QSL(":id"), category->id()); + q.bindValue(QSL(":ordr"), category->sortOrder()); if (!q.exec()) { throw ApplicationException(q.lastError().text()); @@ -1974,9 +1992,26 @@ void DatabaseQueries::createOverwriteFeed(const QSqlDatabase& db, Feed* feed, in if (feed->id() <= 0) { // We need to insert feed first. + if (feed->sortOrder() < 0) { + q.prepare(QSL("SELECT MAX(ordr) FROM Feeds WHERE account_id = :account_id AND category = :category;")); + q.bindValue(QSL(":account_id"), account_id); + q.bindValue(QSL(":category"), parent_id); + + if (!q.exec()) { + throw ApplicationException(q.lastError().text()); + } + + q.next(); + + int next_order = (q.value(0).isNull() ? -1 : q.value(0).toInt()) + 1; + + feed->setSortOrder(next_order); + q.finish(); + } + q.prepare(QSL("INSERT INTO " - "Feeds (title, date_created, category, update_type, update_interval, account_id, custom_id) " - "VALUES ('new', 0, 0, 0, 1, %1, 'new');").arg(QString::number(account_id))); + "Feeds (title, ordr, date_created, category, update_type, update_interval, account_id, custom_id) " + "VALUES ('new', 0, 0, 0, 0, 1, %1, 'new');").arg(QString::number(account_id))); if (!q.exec()) { throw ApplicationException(q.lastError().text()); @@ -1991,7 +2026,7 @@ void DatabaseQueries::createOverwriteFeed(const QSqlDatabase& db, Feed* feed, in } q.prepare("UPDATE Feeds " - "SET title = :title, description = :description, date_created = :date_created, " + "SET title = :title, ordr = :ordr, description = :description, date_created = :date_created, " " icon = :icon, category = :category, source = :source, update_type = :update_type, " " update_interval = :update_interval, is_off = :is_off, open_articles = :open_articles, " " account_id = :account_id, custom_id = :custom_id, custom_data = :custom_data " @@ -2007,6 +2042,7 @@ void DatabaseQueries::createOverwriteFeed(const QSqlDatabase& db, Feed* feed, in q.bindValue(QSL(":account_id"), account_id); q.bindValue(QSL(":custom_id"), feed->customId()); q.bindValue(QSL(":id"), feed->id()); + q.bindValue(QSL(":ordr"), feed->sortOrder()); q.bindValue(QSL(":is_off"), feed->isSwitchedOff()); q.bindValue(QSL(":open_articles"), feed->openArticlesDirectly()); @@ -2024,9 +2060,22 @@ void DatabaseQueries::createOverwriteAccount(const QSqlDatabase& db, ServiceRoot QSqlQuery q(db); if (account->accountId() <= 0) { - // We need to insert account first. - q.prepare(QSL("INSERT INTO Accounts (type) " - "VALUES (:type);")); + // We need to insert account and generate sort order first. + if (account->sortOrder() < 0) { + if (!q.exec(QSL("SELECT MAX(ordr) FROM Accounts;"))) { + throw ApplicationException(q.lastError().text()); + } + + q.next(); + + int next_order = (q.value(0).isNull() ? -1 : q.value(0).toInt()) + 1; + + account->setSortOrder(next_order); + q.finish(); + } + + q.prepare(QSL("INSERT INTO Accounts (ordr, type) " + "VALUES (0, :type);")); q.bindValue(QSL(":type"), account->code()); if (!q.exec()) { @@ -2042,7 +2091,7 @@ void DatabaseQueries::createOverwriteAccount(const QSqlDatabase& db, ServiceRoot q.prepare(QSL("UPDATE Accounts " "SET proxy_type = :proxy_type, proxy_host = :proxy_host, proxy_port = :proxy_port, " - " proxy_username = :proxy_username, proxy_password = :proxy_password, " + " proxy_username = :proxy_username, proxy_password = :proxy_password, ordr = :ordr, " " custom_data = :custom_data " "WHERE id = :id")); q.bindValue(QSL(":proxy_type"), proxy.type()); @@ -2051,6 +2100,7 @@ void DatabaseQueries::createOverwriteAccount(const QSqlDatabase& db, ServiceRoot q.bindValue(QSL(":proxy_username"), proxy.user()); q.bindValue(QSL(":proxy_password"), TextFactory::encrypt(proxy.password())); q.bindValue(QSL(":id"), account->accountId()); + q.bindValue(QSL(":ordr"), account->sortOrder()); auto custom_data = account->customDatabaseData(); QString serialized_custom_data = serializeCustomData(custom_data); @@ -2093,6 +2143,81 @@ bool DatabaseQueries::deleteCategory(const QSqlDatabase& db, int id) { return q.exec(); } +void DatabaseQueries::moveItem(RootItem* item, bool move_top, bool move_bottom, int move_index, const QSqlDatabase& db) { + switch (item->kind()) { + case RootItem::Kind::Feed: + moveFeed(item->toFeed(), move_top, move_bottom, move_index, db); + break; + + case RootItem::Kind::Category: + break; + + case RootItem::Kind::ServiceRoot: + + break; + + default: + return; + } +} + +void DatabaseQueries::moveFeed(Feed* feed, bool move_top, bool move_bottom, int move_index, const QSqlDatabase& db) { + if (feed->sortOrder() == move_index || /* Item is already sorted OK. */ + (!move_top && !move_bottom && move_index < 0 ) || /* Order cannot be smaller than 0 if we do not move to begin/end. */ + (move_top && feed->sortOrder() == 0)) { /* Item is already on top. */ + return; + } + + QSqlQuery q(db); + + q.prepare(QSL("SELECT MAX(ordr) FROM Feeds WHERE account_id = :account_id AND category = :category;")); + q.bindValue(QSL(":account_id"), feed->getParentServiceRoot()->accountId()); + q.bindValue(QSL(":category"), feed->parent()->id()); + + int max_sort_order; + + if (q.exec() && q.next()) { + max_sort_order = q.value(0).toInt(); + } + else { + throw ApplicationException(q.lastError().text()); + } + + if (max_sort_order == 0 || /* We only have 1 item, nothing to sort. */ + max_sort_order == feed->sortOrder() || /* Item is already sorted OK. */ + move_index > max_sort_order) { /* Cannot move past biggest sort order. */ + return; + } + + if (move_top) { + move_index = 0; + } + else if (move_bottom) { + move_index = max_sort_order; + } + + int move_low = qMin(move_index, feed->sortOrder()); + int move_high = qMax(move_index, feed->sortOrder()); + + if (feed->sortOrder() > move_index) { + q.prepare(QSL("UPDATE Feeds SET ordr = ordr + 1 " + "WHERE account_id = :account_id AND category = :category AND ordr < :move_high AND ordr >= :move_low;")); + } + else { + q.prepare(QSL("UPDATE Feeds SET ordr = ordr - 1 " + "WHERE account_id = :account_id AND category = :category AND ordr > :move_low AND ordr <= :move_high;")); + } + + q.bindValue(QSL(":account_id"), feed->getParentServiceRoot()->accountId()); + q.bindValue(QSL(":category"), feed->parent()->id()); + q.bindValue(QSL(":move_low"), move_low); + q.bindValue(QSL(":move_high"), move_high); + + if (!q.exec()) { + throw ApplicationException(q.lastError().text()); + } +} + MessageFilter* DatabaseQueries::addMessageFilter(const QSqlDatabase& db, const QString& title, const QString& script) { if (!db.driver()->hasFeature(QSqlDriver::DriverFeature::LastInsertId)) { diff --git a/src/librssguard/database/databasequeries.h b/src/librssguard/database/databasequeries.h index f028564ed..ce9455f72 100644 --- a/src/librssguard/database/databasequeries.h +++ b/src/librssguard/database/databasequeries.h @@ -134,6 +134,10 @@ class DatabaseQueries { static Assignment getFeeds(const QSqlDatabase& db, const QList& global_filters, int account_id, bool* ok = nullptr); + // Item order methods. + static void moveItem(RootItem* item, bool move_top, bool move_bottom, int move_index, const QSqlDatabase& db); + static void moveFeed(Feed* feed, bool move_top, bool move_bottom, int move_index, const QSqlDatabase& db); + // Message filters operators. static bool purgeLeftoverMessageFilterAssignments(const QSqlDatabase& db, int account_id); static MessageFilter* addMessageFilter(const QSqlDatabase& db, const QString& title, const QString& script); @@ -167,6 +171,7 @@ QList DatabaseQueries::getAccounts(const QSqlDatabase& db, const Q // Load common data. root->setAccountId(query.value(QSL("id")).toInt()); + root->setSortOrder(query.value(QSL("ordr")).toInt()); QNetworkProxy proxy(QNetworkProxy::ProxyType(query.value(QSL("proxy_type")).toInt()), query.value(QSL("proxy_host")).toString(), @@ -232,6 +237,7 @@ Assignment DatabaseQueries::getCategories(const QSqlDatabase& db, int account_id auto* cat = static_cast(pair.second); cat->setId(query_categories.value(CAT_DB_ID_INDEX).toInt()); + cat->setSortOrder(query_categories.value(CAT_DB_ORDER_INDEX).toInt()); cat->setCustomId(query_categories.value(CAT_DB_CUSTOM_ID_INDEX).toString()); if (cat->customId().isEmpty()) { @@ -287,6 +293,7 @@ Assignment DatabaseQueries::getFeeds(const QSqlDatabase& db, // Load common data. feed->setTitle(query.value(FDS_DB_TITLE_INDEX).toString()); feed->setId(query.value(FDS_DB_ID_INDEX).toInt()); + feed->setSortOrder(query.value(FDS_DB_ORDER_INDEX).toInt()); feed->setSource(query.value(FDS_DB_SOURCE_INDEX).toString()); feed->setCustomId(query.value(FDS_DB_CUSTOM_ID_INDEX).toString()); diff --git a/src/librssguard/definitions/definitions.h b/src/librssguard/definitions/definitions.h index a8400131c..d06ce8352 100644 --- a/src/librssguard/definitions/definitions.h +++ b/src/librssguard/definitions/definitions.h @@ -243,29 +243,31 @@ // Indexes of columns as they are DEFINED IN THE TABLE for CATEGORIES. #define CAT_DB_ID_INDEX 0 -#define CAT_DB_PARENT_ID_INDEX 1 -#define CAT_DB_TITLE_INDEX 2 -#define CAT_DB_DESCRIPTION_INDEX 3 -#define CAT_DB_DCREATED_INDEX 4 -#define CAT_DB_ICON_INDEX 5 -#define CAT_DB_ACCOUNT_ID_INDEX 6 -#define CAT_DB_CUSTOM_ID_INDEX 7 +#define CAT_DB_ORDER_INDEX 1 +#define CAT_DB_PARENT_ID_INDEX 2 +#define CAT_DB_TITLE_INDEX 3 +#define CAT_DB_DESCRIPTION_INDEX 4 +#define CAT_DB_DCREATED_INDEX 5 +#define CAT_DB_ICON_INDEX 6 +#define CAT_DB_ACCOUNT_ID_INDEX 7 +#define CAT_DB_CUSTOM_ID_INDEX 8 // Indexes of columns as they are DEFINED IN THE TABLE for FEEDS. #define FDS_DB_ID_INDEX 0 -#define FDS_DB_TITLE_INDEX 1 -#define FDS_DB_DESCRIPTION_INDEX 2 -#define FDS_DB_DCREATED_INDEX 3 -#define FDS_DB_ICON_INDEX 4 -#define FDS_DB_CATEGORY_INDEX 5 -#define FDS_DB_SOURCE_INDEX 6 -#define FDS_DB_UPDATE_TYPE_INDEX 7 -#define FDS_DB_UPDATE_INTERVAL_INDEX 8 -#define FDS_DB_IS_OFF_INDEX 9 -#define FDS_DB_OPEN_ARTICLES_INDEX 10 -#define FDS_DB_ACCOUNT_ID_INDEX 11 -#define FDS_DB_CUSTOM_ID_INDEX 12 -#define FDS_DB_CUSTOM_DATA_INDEX 13 +#define FDS_DB_ORDER_INDEX 1 +#define FDS_DB_TITLE_INDEX 2 +#define FDS_DB_DESCRIPTION_INDEX 3 +#define FDS_DB_DCREATED_INDEX 4 +#define FDS_DB_ICON_INDEX 5 +#define FDS_DB_CATEGORY_INDEX 6 +#define FDS_DB_SOURCE_INDEX 7 +#define FDS_DB_UPDATE_TYPE_INDEX 8 +#define FDS_DB_UPDATE_INTERVAL_INDEX 9 +#define FDS_DB_IS_OFF_INDEX 10 +#define FDS_DB_OPEN_ARTICLES_INDEX 11 +#define FDS_DB_ACCOUNT_ID_INDEX 12 +#define FDS_DB_CUSTOM_ID_INDEX 13 +#define FDS_DB_CUSTOM_DATA_INDEX 14 // Indexes of columns for feed models. #define FDS_MODEL_TITLE_INDEX 0 diff --git a/src/librssguard/gui/dialogs/formmain.cpp b/src/librssguard/gui/dialogs/formmain.cpp index f6528fb40..0965c4ed1 100644 --- a/src/librssguard/gui/dialogs/formmain.cpp +++ b/src/librssguard/gui/dialogs/formmain.cpp @@ -196,6 +196,7 @@ QList FormMain::allActions() const { actions << m_ui->m_actionClearSelectedItems; actions << m_ui->m_actionClearAllItems; actions << m_ui->m_actionShowOnlyUnreadItems; + actions << m_ui->m_actionSortFeedsAlphabetically; actions << m_ui->m_actionShowTreeBranches; actions << m_ui->m_actionAutoExpandItemsWhenSelected; actions << m_ui->m_actionShowOnlyUnreadMessages; @@ -216,6 +217,10 @@ QList FormMain::allActions() const { actions << m_ui->m_actionServiceDelete; actions << m_ui->m_actionCleanupDatabase; actions << m_ui->m_actionAddFeedIntoSelectedItem; + actions << m_ui->m_actionFeedMoveUp; + actions << m_ui->m_actionFeedMoveDown; + actions << m_ui->m_actionFeedMoveTop; + actions << m_ui->m_actionFeedMoveBottom; actions << m_ui->m_actionAddCategoryIntoSelectedItem; actions << m_ui->m_actionViewSelectedItemsNewspaperMode; actions << m_ui->m_actionSelectNextItem; @@ -475,6 +480,7 @@ void FormMain::updateFeedButtonsAvailability() { const bool feed_selected = anything_selected && selected_item->kind() == RootItem::Kind::Feed; const bool category_selected = anything_selected && selected_item->kind() == RootItem::Kind::Category; const bool service_selected = anything_selected && selected_item->kind() == RootItem::Kind::ServiceRoot; + const bool manual_feed_sort = !m_ui->m_actionSortFeedsAlphabetically->isChecked(); m_ui->m_actionStopRunningItemsUpdate->setEnabled(is_update_running); m_ui->m_actionBackupDatabaseSettings->setEnabled(!critical_action_running); @@ -498,6 +504,11 @@ void FormMain::updateFeedButtonsAvailability() { m_ui->m_menuAddItem->setEnabled(!critical_action_running); m_ui->m_menuAccounts->setEnabled(!critical_action_running); m_ui->m_menuRecycleBin->setEnabled(!critical_action_running); + + m_ui->m_actionFeedMoveUp->setEnabled(manual_feed_sort &&(feed_selected || category_selected || service_selected)); + m_ui->m_actionFeedMoveDown->setEnabled(manual_feed_sort &&(feed_selected || category_selected || service_selected)); + m_ui->m_actionFeedMoveTop->setEnabled(manual_feed_sort &&(feed_selected || category_selected || service_selected)); + m_ui->m_actionFeedMoveBottom->setEnabled(manual_feed_sort &&(feed_selected || category_selected || service_selected)); } void FormMain::switchVisibility(bool force_hide) { @@ -595,6 +606,7 @@ void FormMain::setupIcons() { m_ui->m_actionSelectNextMessage->setIcon(icon_theme_factory->fromTheme(QSL("arrow-down"))); m_ui->m_actionSelectPreviousMessage->setIcon(icon_theme_factory->fromTheme(QSL("arrow-up"))); m_ui->m_actionSelectNextUnreadMessage->setIcon(icon_theme_factory->fromTheme(QSL("mail-mark-unread"))); + m_ui->m_actionSortFeedsAlphabetically->setIcon(icon_theme_factory->fromTheme(QSL("format-text-bold"))); m_ui->m_actionShowOnlyUnreadItems->setIcon(icon_theme_factory->fromTheme(QSL("mail-mark-unread"))); m_ui->m_actionShowOnlyUnreadMessages->setIcon(icon_theme_factory->fromTheme(QSL("mail-mark-unread"))); m_ui->m_actionExpandCollapseItem->setIcon(icon_theme_factory->fromTheme(QSL("format-indent-more"))); @@ -609,6 +621,11 @@ void FormMain::setupIcons() { m_ui->m_actionAddCategoryIntoSelectedItem->setIcon(icon_theme_factory->fromTheme(QSL("folder"))); m_ui->m_actionMessageFilters->setIcon(icon_theme_factory->fromTheme(QSL("view-list-details"))); + m_ui->m_actionFeedMoveUp->setIcon(icon_theme_factory->fromTheme(QSL("arrow-up"))); + m_ui->m_actionFeedMoveDown->setIcon(icon_theme_factory->fromTheme(QSL("arrow-down"))); + m_ui->m_actionFeedMoveTop->setIcon(icon_theme_factory->fromTheme(QSL("arrow-up-double"))); + m_ui->m_actionFeedMoveBottom->setIcon(icon_theme_factory->fromTheme(QSL("arrow-down-double"))); + // Tabs & web browser. m_ui->m_actionTabNewWebBrowser->setIcon(icon_theme_factory->fromTheme(QSL("tab-new"))); m_ui->m_actionTabsCloseAll->setIcon(icon_theme_factory->fromTheme(QSL("window-close"))); @@ -667,6 +684,8 @@ void FormMain::loadSize() { m_ui->m_actionSwitchStatusBar->setChecked(settings->value(GROUP(GUI), SETTING(GUI::StatusBarVisible)).toBool()); // Other startup GUI-related settings. + m_ui->m_actionSortFeedsAlphabetically->setChecked(settings->value(GROUP(Feeds), + SETTING(Feeds::SortAlphabetically)).toBool()); m_ui->m_actionShowOnlyUnreadItems->setChecked(settings->value(GROUP(Feeds), SETTING(Feeds::ShowOnlyUnreadFeeds)).toBool()); m_ui->m_actionShowTreeBranches->setChecked(settings->value(GROUP(Feeds), @@ -858,6 +877,8 @@ void FormMain::createConnections() { tabWidget()->feedMessageViewer(), &FeedMessageViewer::switchMessageSplitterOrientation); connect(m_ui->m_actionShowOnlyUnreadItems, &QAction::toggled, tabWidget()->feedMessageViewer(), &FeedMessageViewer::toggleShowOnlyUnreadFeeds); + connect(m_ui->m_actionSortFeedsAlphabetically, &QAction::toggled, + tabWidget()->feedMessageViewer()->feedsView(), &FeedsView::toggleFeedSortingMode); connect(m_ui->m_actionShowTreeBranches, &QAction::toggled, tabWidget()->feedMessageViewer(), &FeedMessageViewer::toggleShowFeedTreeBranches); connect(m_ui->m_actionAutoExpandItemsWhenSelected, &QAction::toggled, @@ -876,6 +897,8 @@ void FormMain::createConnections() { qApp->feedReader()->showMessageFiltersManager(); tabWidget()->feedMessageViewer()->messagesView()->reloadSelections(); }); + connect(m_ui->m_actionFeedMoveUp, &QAction::triggered, + tabWidget()->feedMessageViewer()->feedsView(), &FeedsView::moveSelectedItemUp); } void FormMain::backupDatabaseSettings() { diff --git a/src/librssguard/gui/dialogs/formmain.ui b/src/librssguard/gui/dialogs/formmain.ui index ecd059638..61f646c69 100644 --- a/src/librssguard/gui/dialogs/formmain.ui +++ b/src/librssguard/gui/dialogs/formmain.ui @@ -107,6 +107,16 @@ &Add item + + + &Move + + + + + + + @@ -116,11 +126,13 @@ + + @@ -853,6 +865,37 @@ Open in internal browser (no new tab) + + + true + + + true + + + &Sort alphabetically + + + + + Move &up + + + + + Move to &top + + + + + Move &down + + + + + Move to &bottom + + diff --git a/src/librssguard/gui/feedsview.cpp b/src/librssguard/gui/feedsview.cpp index 8ccd9d760..76373b37d 100644 --- a/src/librssguard/gui/feedsview.cpp +++ b/src/librssguard/gui/feedsview.cpp @@ -288,6 +288,10 @@ void FeedsView::deleteSelectedItem() { qApp->feedUpdateLock()->unlock(); } +void FeedsView::moveSelectedItemUp() { + m_sourceModel->changeSortOrder(selectedItem(), false, false, selectedItem()->sortOrder() - 1); +} + void FeedsView::markSelectedItemReadStatus(RootItem::ReadStatus read) { m_sourceModel->markItemRead(selectedItem(), read); } @@ -512,6 +516,11 @@ void FeedsView::filterItems(const QString& pattern) { } } +void FeedsView::toggleFeedSortingMode(bool sort_alphabetically) { + setSortingEnabled(sort_alphabetically); + m_proxyModel->setSortAlphabetically(sort_alphabetically); +} + void FeedsView::onIndexExpanded(const QModelIndex& idx) { qDebugNN << LOGSEC_GUI << "Feed list item expanded - " << m_proxyModel->data(idx).toString(); @@ -698,6 +707,15 @@ QMenu* FeedsView::initializeContextMenuFeeds(RootItem* clicked_item) { m_contextMenuFeeds->addAction(qApp->mainForm()->m_ui->m_actionAddFeedIntoSelectedItem); } + if (!qApp->settings()->value(GROUP(Feeds), + SETTING(Feeds::SortAlphabetically)).toBool()) { + m_contextMenuFeeds->addSeparator(); + m_contextMenuFeeds->addAction(qApp->mainForm()->m_ui->m_actionFeedMoveUp); + m_contextMenuFeeds->addAction(qApp->mainForm()->m_ui->m_actionFeedMoveDown); + m_contextMenuFeeds->addAction(qApp->mainForm()->m_ui->m_actionFeedMoveTop); + m_contextMenuFeeds->addAction(qApp->mainForm()->m_ui->m_actionFeedMoveBottom); + } + if (!specific_actions.isEmpty()) { m_contextMenuFeeds->addSeparator(); m_contextMenuFeeds->addActions(specific_actions); diff --git a/src/librssguard/gui/feedsview.h b/src/librssguard/gui/feedsview.h index 58a0ac785..e715ad94e 100644 --- a/src/librssguard/gui/feedsview.h +++ b/src/librssguard/gui/feedsview.h @@ -66,6 +66,9 @@ class RSSGUARD_DLLSPEC FeedsView : public BaseTreeView { void editSelectedItem(); void deleteSelectedItem(); + // Sort order manipulations. + void moveSelectedItemUp(); + // Selects next/previous item (feed/category) in the list. void selectNextItem(); void selectPreviousItem(); @@ -76,6 +79,7 @@ class RSSGUARD_DLLSPEC FeedsView : public BaseTreeView { void switchVisibility(); void filterItems(const QString& pattern); + void toggleFeedSortingMode(bool sort_alphabetically); void invalidateReadFeedsFilter(bool set_new_value = false, bool show_unread_only = false); signals: diff --git a/src/librssguard/miscellaneous/settings.cpp b/src/librssguard/miscellaneous/settings.cpp index 12ba0df35..66369d806 100644 --- a/src/librssguard/miscellaneous/settings.cpp +++ b/src/librssguard/miscellaneous/settings.cpp @@ -88,6 +88,9 @@ DVALUE(double) Feeds::FeedsUpdateStartupDelayDef = STARTUP_UPDATE_DELAY; DKEY Feeds::ShowOnlyUnreadFeeds = "show_only_unread_feeds"; DVALUE(bool) Feeds::ShowOnlyUnreadFeedsDef = false; +DKEY Feeds::SortAlphabetically = "sort_alphabetically"; +DVALUE(bool) Feeds::SortAlphabeticallyDef = true; + DKEY Feeds::ShowTreeBranches = "show_tree_branches"; DVALUE(bool) Feeds::ShowTreeBranchesDef = true; diff --git a/src/librssguard/miscellaneous/settings.h b/src/librssguard/miscellaneous/settings.h index 69df38f9c..afd7b2599 100644 --- a/src/librssguard/miscellaneous/settings.h +++ b/src/librssguard/miscellaneous/settings.h @@ -93,6 +93,9 @@ namespace Feeds { KEY ShowOnlyUnreadFeeds; VALUE(bool) ShowOnlyUnreadFeedsDef; + KEY SortAlphabetically; + VALUE(bool) SortAlphabeticallyDef; + KEY ShowTreeBranches; VALUE(bool) ShowTreeBranchesDef; diff --git a/src/librssguard/services/abstract/rootitem.cpp b/src/librssguard/services/abstract/rootitem.cpp index 3c14bdeed..198b3f05e 100644 --- a/src/librssguard/services/abstract/rootitem.cpp +++ b/src/librssguard/services/abstract/rootitem.cpp @@ -16,7 +16,7 @@ RootItem::RootItem(RootItem* parent_item) : QObject(nullptr), m_kind(RootItem::Kind::Root), m_id(NO_PARENT_CATEGORY), m_customId(QL1S("")), m_title(QString()), m_description(QString()), m_creationDate(QDateTime::currentDateTimeUtc()), - m_keepOnTop(false), m_childItems(QList()), m_parentItem(parent_item) {} + m_keepOnTop(false), m_sortOrder(NO_PARENT_CATEGORY), m_childItems(QList()), m_parentItem(parent_item) {} RootItem::RootItem(const RootItem& other) : RootItem(nullptr) { setTitle(other.title()); @@ -24,6 +24,7 @@ RootItem::RootItem(const RootItem& other) : RootItem(nullptr) { setCustomId(other.customId()); setIcon(other.icon()); setKeepOnTop(other.keepOnTop()); + setSortOrder(other.sortOrder()); // NOTE: We do not need to clone childs, because that would mean that // either source or target item tree would get corrupted. @@ -564,6 +565,14 @@ void RootItem::setKeepOnTop(bool keep_on_top) { m_keepOnTop = keep_on_top; } +int RootItem::sortOrder() const { + return m_sortOrder; +} + +void RootItem::setSortOrder(int sort_order) { + m_sortOrder = sort_order; +} + bool RootItem::removeChild(int index) { if (index >= 0 && index < m_childItems.size()) { m_childItems.removeAt(index); diff --git a/src/librssguard/services/abstract/rootitem.h b/src/librssguard/services/abstract/rootitem.h index 1d2933d6a..119e0c4d0 100644 --- a/src/librssguard/services/abstract/rootitem.h +++ b/src/librssguard/services/abstract/rootitem.h @@ -199,6 +199,20 @@ class RSSGUARD_DLLSPEC RootItem : public QObject { bool keepOnTop() const; void setKeepOnTop(bool keep_on_top); + // Sort order, when items in feeds list are sorted manually. + // + // NOTE: This is only used for "Account", "Category" and "Feed" classes + // which can be manually sorted. Other types like "Label" cannot be + // automatically sorted and are always sorted by title. + // + // Sort order number cannot be negative but order of list of items with same + // parent MUST form continuous series AND start with zero, for example: + // 0, 1, 2, 3, 4, ... + // + // NOTE: This is checked with DatabaseQueries::fixupOrders() method on app startup. + int sortOrder() const; + void setSortOrder(int sort_order); + private: RootItem::Kind m_kind; int m_id; @@ -208,6 +222,7 @@ class RSSGUARD_DLLSPEC RootItem : public QObject { QIcon m_icon; QDateTime m_creationDate; bool m_keepOnTop; + int m_sortOrder; QList m_childItems; RootItem* m_parentItem; };