Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use tree for feeds in RSS downloader dialog. Closes #6281. #6345

Closed
wants to merge 8 commits into from

Conversation

magao
Copy link
Contributor

@magao magao commented Feb 5, 2017

This PR builds on PR #6326. It changes the RSS downloader dialog to use a tree for its feed list, as discussed in issue #6281.

This PR also introduces a new form of Natural Sort - the "case semi-sensitive" sort. This is a case insensitive sort, except where 2 strings compare equal ignoring case, in which case the strings are compared using a case-sensitive sort. This ensures a consistent ordering.

This PR also introduces subclasses of QListWidgetItem and QTreeWidgetItem that sort via natural sort (defaults to case semi-sensitive, but can be specified as case-sensitive or case-insensitive).

The RSS tab and downloader dialogs use these classes to sort, with a further enhancement that folders sort first (and for the RSS tab, the Unread item sorts before all others).

feed_tree

If there are no visible folders then the expander decorations are hidden, resulting in effectively the same UI as current.

@magao
Copy link
Contributor Author

magao commented Feb 5, 2017

This could potentially also close issue #1805 - as I noted there, with this PR you could organise your feeds into folders (maybe a single top-level folder) so you can select multiple feeds at once.

Also #3679 as if this PR is merged then I think #3679 becomes redundant.

@magao magao mentioned this pull request Feb 5, 2017
@Chocobo1
Copy link
Member

Chocobo1 commented Feb 5, 2017

This PR also introduces a new form of Natural Sort - the "case semi-sensitive" sort.

That's what I've tried to achieved when I was cleaning that up a long time ago. And I didn't opt to do so before is because I thought 2-pass for doing sorting is sorta a waste of computing resource, now I doubt my views.
Also I tried to impl the same sorting after reading your idea :) https://github.com/Chocobo1/qBittorrent/commits/sort

Can you also enlighten me what is the advantage of SortableListWidgetItem (and the other one)?
AFAIK, the sorting is done in the Qt Model part so why a SortableTreeWidgetItem::operator< will benefit is not so clear for me, I mean you could just simply plug in Utils::String::naturalCompareCaseSensitive where it's needed...

int posL = 0;
int posR = 0;
while (true) {
while (true) {
if ((posL == left.size()) || (posR == right.size()))
return (left.size() < right.size()); // when a shorter string is another string's prefix, shorter string place before longer string
return (left.size() < right.size()) ? -1 : 1; // when a shorter string is another string's prefix, shorter string place before longer string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should just use subtraction here and below.

Copy link
Contributor Author

@magao magao Feb 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that ... probably better. Although in theory that does potentially suffer underflow errors if right.size() is large enough. Would need to first cast both to long, then the result back to int. Not sure if that would be more or less efficient than a branch.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You lost me here, how could the same type with only >=0 values, subtraction causes an integer underflow? care to provide an example here?

Not sure if that would be more or less efficient than a branch.

In human perspective on computer, should be faster; in reality, depends on how good the compiler optimization is...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

D'oh! For some reason it totally slipped my mind that they're both always positive values ... so no chance of underflow.

Without casting subtraction should definitely be more efficient.

thread_local NaturalCompare nCmp(true);
return nCmp(left, right);
#endif
return NaturalCompare::naturalCompareCaseSensitive(left, right) < 0;
Copy link
Member

@Chocobo1 Chocobo1 Feb 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't really have to create this function in order to let Utils::String::naturalCompareCaseSemiSensitive looks nice... or you could just propagate the return int up as I've done here: Chocobo1@20964ce
Just some ideas though...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't to make the functions look nice, but to avoid:

  1. Duplicating the code again;

  2. Having extra thread-locals when we only need the 2.

@magao
Copy link
Contributor Author

magao commented Feb 5, 2017

The cases where it would need to fall back to a case-sensitive sort are likely to be rare - I don't see the extra sort in those cases to be a performance killer. There may be cases where we genuinely want a case-insensitive sort e.g. a case-insensitive container, so having the case-insensitive sort automatically fall back to case-sensitive as a tie-breaker may not be correct in all cases (hence why I created a separate function).

There are some widgets where you can't change the model (setModel() is private) - including QTreeWidget and QListWidget. The recommended way to sort items of these widgets is to override operator<() on the *WidgetItem (I've seen some code which directly calls like ui.treeWidget->QTreeWidget::setModel(proxyModel); but that's always followed by a "don't do that" ...

Since I was going to use the behaviour in multiple places, I thought it best to create utility classes.

@sledgehammer999 sledgehammer999 added the RSS RSS-related issues/changes label Feb 5, 2017
@Chocobo1
Copy link
Member

Chocobo1 commented Feb 6, 2017

so having the case-insensitive sort automatically fall back to case-sensitive as a tie-breaker may not be correct in all cases

I'm curious, for example?

but that's always followed by a "don't do that" ...

This one too, a link to somewhere that explains this?

@magao
Copy link
Contributor Author

magao commented Feb 6, 2017

so having the case-insensitive sort automatically fall back to case-sensitive as a tie-breaker may not be correct in all cases

I'm curious, for example?

I gave one - a case-insensitive container. Another example would be anything on a case-insensitive filesystem. In both of these cases, multiple entries with differing cases should compare equal.

I guess it comes down to if I have a function that claims to do case-insensitive comparisons, then that's exactly what I expect it to do. The definition of "case-sensitive" might need explaining (in particular, whether it's locale-aware or not, follows unicode case definitions, etc).

OTOH, if the functions were named in such a way as to make it clear that they're specifically for sorting (and commented that case-insensitive uses a tie-breaker), I wouldn't have a problem with it.

I should note that currently all uses of naturalCompareCaseInsensitive are sorting, so changing the behaviour shouldn't have any negative consequences to the existing codebase.

Re: sorting items in a QListWidget or QTreeWidget:

https://stackoverflow.com/questions/8493671/sorting-a-qlistwidget-using-sortitems
https://stackoverflow.com/questions/11335602/sort-a-qtreewidgetitem-by-a-data-value#11764852
http://www.qtcentre.org/threads/2904-how-to-use-QSortFilterProxyModel-with-QtreeWidget
https://forum.qt.io/topic/61239/how-does-qtreewidgetitem-sortchildren-sort
http://www.qtforum.org/article/25281/customize-sorting-of-qtreewidget.html

@Chocobo1
Copy link
Member

Chocobo1 commented Feb 6, 2017

OTOH, if the functions were named in such a way as to make it clear that they're specifically for sorting (and commented that case-insensitive uses a tie-breaker),

I see your point, it's because the functions is called *Compare instead of *LessThan or *Sort, my head is twisting now, maybe you could come up with a good name...

Re: sorting items in a QListWidget or QTreeWidget:

OK, good point, just one more thing, to keep things tidy, you should create a new folder src/gui/widgets/ (for reusable components) and place sortablewidgetitems.* in it, there was some discussions about this before you come along.

void handleFeedCheckStateChange(QListWidgetItem *feed_item);
void updateRuleDefinitionBox();
void handleFeedCheckStateChange(QTreeWidgetItem *feed_item, int column);
void updateRuleDefinitionBox(QListWidgetItem *selected = 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use nullptr instead of 0 (and everywhere else).

@@ -647,20 +649,26 @@ void RSSImp::updateItemInfos(QTreeWidgetItem *item)
void RSSImp::updateFeedIcon(const QString &url, const QString &iconPath)
{
QTreeWidgetItem *item = m_feedList->getTreeItemFromUrl(url);
item->setData(0, Qt::DecorationRole, QVariant(QIcon(iconPath)));

if (item != 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for consistency: if (!item) return;

if (item->parent())
updateItemInfos(item->parent());

if (item != 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for consistency: if (!item) return;


if (item != 0) {
Rss::FeedPtr stream = qSharedPointerCast<Rss::Feed>(m_feedList->getRSSItem(item));
item->setText(0, display_name + QString::fromUtf8(" (") + QString::number(nbUnread) + QString(")"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you do us a favor:

(0, display_name + "  (" + QString::number(nbUnread) + ")");

@@ -0,0 +1,129 @@
/*
* Bittorrent Client using Qt4 and libtorrent.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the 4

@@ -0,0 +1,129 @@
/*
* Bittorrent Client using Qt4 and libtorrent.
* Copyright (C) 2017 Christophe Dumez
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess he doesn't wrote this part, put your name here or "The qBittorrent team".

@@ -0,0 +1,77 @@
/*
* Bittorrent Client using Qt4 and libtorrent.
* Copyright (C) 2017 Christophe Dumez
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same the above 2 lines.

virtual bool operator<(const QTreeWidgetItem &other) const;

private:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redundant empty line

*/

#include "base/utils/string.h"
#include "sortablewidgetitems.h"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#include "sortablewidgetitems.h"
// empty line
#include "base/utils/string.h"

return 0;
}

static int naturalCompareCaseSensitive(const QString &left, const QString &right)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if there is this rule: for each local function, you should create a function prototype

@@ -255,6 +262,10 @@ void RSSImp::deleteSelectedItems()
}
}
m_rssManager->saveStreamList();

foreach (const QString &feed_id, deleted)
m_rssManager->forwardFeedInfosChanged(feed_id, "", 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"" -> QString() or {}

};

AutomatedRssDownloader::FeedListWidgetItem::FeedListWidgetItem(const Rss::FilePtr &rssFile)
: SortableTreeWidgetItem(QStringList() << rssFile->displayName())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SortableTreeWidgetItem{ rssFile->displayName() }

AutomatedRssDownloader::FeedListWidgetItem::FeedListWidgetItem(const Rss::FilePtr &rssFile)
: SortableTreeWidgetItem(QStringList() << rssFile->displayName())
{
bool folder = qSharedPointerDynamicCast<Rss::Folder>(rssFile) != 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nullptr

if (!manager)
return;

QList<Rss::FilePtr> children;
Copy link
Member

@Chocobo1 Chocobo1 Feb 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is personal preference:

const QList<Rss::FilePtr> children = rss_parent ? rss_parent->getContent() : manager->rootFolder()->getContent();

You save a few lines and no loss in readability and the best: you can stick on that const.

bool all_enabled = false;
disconnectRuleFeedSlots();

QList<QListWidgetItem *> selection;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here too.

bool anyFolderVisible = false;
QTreeWidgetItemIterator it(ui->listFeeds);

while (*it) {
Copy link
Member

@Chocobo1 Chocobo1 Feb 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible?

for (; *it; ++it) {

break;

if (!text.isEmpty()) {
QStringList tokens;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could merge with the following lines...

@@ -659,7 +844,8 @@ void AutomatedRssDownloader::updateMustNotLineValidity()
void AutomatedRssDownloader::updateEpisodeFilterValidity()
{
const QString text = ui->lineEFilter->text();
bool valid = m_episodeRegex->indexIn(text) != -1;
bool valid = text.isEmpty() || m_episodeRegex->indexIn(text) != -1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add parentheses

break;

if (!text.isEmpty()) {
QStringList tokens;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here.

m_editedRule = selection.first();
Rss::DownloadRulePtr rule = getCurrentRule();

if (!selected && (selection.count() == 1))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... I find a little hard to follow what the code is trying to do, is it possible to refactor it?
I mean there are 2 boolean conditions so there are 4 outcomes, reading the code I cannot conclude that all possibilities are covered.
Or maybe: !selection.empty()?

Copy link
Contributor Author

@magao magao Feb 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no selected parameter was passed into the function, and there is exactly one object selected in the UI, we set that as the selection - it's the rule we're currently editing. If there are either zero rules selected, or more than one, we want to clear the rule definition box and make sure we don't think we're editing a rule.

Currently selected will only be passed in when creating a new rule. updateFeedList() later ensures that it's actually selected in the UI.

The next branch if (selected) is only followed if there is exactly one selected item.

Note that this code has already been merged as part of PR #6326.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
RSS RSS-related issues/changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants